Check the #[\Override] attribute on class constants and enum cases #33744
Workflow file for this run
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
| # https://help.github.com/en/categories/automating-your-workflow-with-github-actions | |
| name: "Compile PHAR" | |
| on: | |
| pull_request: | |
| push: | |
| branches: | |
| - "2.3.x" | |
| tags: | |
| - '2.3.*' | |
| concurrency: | |
| group: phar-${{ github.ref }} # will be canceled on subsequent pushes in both branches and pull requests | |
| cancel-in-progress: true | |
| # Read-only GITHUB_TOKEN everywhere; jobs needing more declare their own | |
| # block. The commit job pushes with a PAT, not the workflow token. | |
| permissions: | |
| contents: read | |
| env: | |
| # Turbo binaries phpstan/phpstan used to carry but no compile leg builds any | |
| # more (paths under turbo-ext/, shell globs). The commit job deletes them | |
| # from the dist; without this, the torn-set guard there would refuse every | |
| # version bump, since a retired binary has no freshly built replacement. | |
| # Linux ZTS binaries are built from PHP 8.6 on again (see turbo-compile), | |
| # so only the older minors' are retired. | |
| TURBO_RETIRED_BINARIES: "linux-gnu-x86_64/phpstan_turbo-8.[345]-zts.so linux-gnu-arm64/phpstan_turbo-8.[345]-zts.so" | |
| # The php-parser version whose grammar tables and semantic actions the | |
| # native parser engine (turbo-ext/src/parser/) was ported against. | |
| SUPPORTED_PHP_PARSER_VERSION: "v5.9.0" | |
| # Composer for the turbo container legs, pinned by checksum. The distro | |
| # composers are unusable there: jammy's 2.2.6 fatals under PHP 8.5, and | |
| # Alpine's /usr/bin/composer is a shell wrapper hardwired to the | |
| # distro-default PHP. | |
| COMPOSER_VERSION: "2.9.2" | |
| COMPOSER_PHAR_SHA256: "471f2d857abf0ec18af7b055e61472214d91adb24f9bdbbb864c1c64faad7dd6" | |
| # setup-php installs an 8.6 nightly, which has no matching Windows devel pack. | |
| PHP86_WINDOWS_VERSION: "8.6.0RC2" | |
| jobs: | |
| compiler-tests: | |
| name: "Compiler Tests" | |
| runs-on: "ubuntu-latest" | |
| timeout-minutes: 60 | |
| permissions: | |
| contents: read # actions/checkout of this repository | |
| pull-requests: read # dorny/paths-filter lists the PR's changed files through the API | |
| outputs: | |
| checksum: ${{ steps.checksum.outputs.md5 }} | |
| compiler_changed: ${{ steps.changes.outputs.compiler }} | |
| steps: | |
| - name: Harden the runner (Audit all outbound calls) | |
| uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 | |
| with: | |
| egress-policy: audit | |
| - name: "Checkout" | |
| uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 | |
| with: | |
| persist-credentials: false | |
| fetch-depth: 0 | |
| - name: "Install PHP" | |
| uses: "shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240" # 2.37.2 | |
| with: | |
| coverage: "none" | |
| php-version: "8.2" | |
| extensions: mbstring, intl | |
| - uses: "ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda" # v4.0.0 | |
| with: | |
| ignore-cache: true | |
| # only sebastian/diff ^4 supports PHP 7.4 so we need that in the PHAR | |
| - name: "Downgrade PHPUnit" | |
| run: "composer require --dev phpunit/phpunit:^9.6 sebastian/diff:^4.0 doctrine/instantiator:^1.0 --update-with-dependencies --ignore-platform-reqs" | |
| - name: "Install compiler dependencies" | |
| uses: "ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda" # v4.0.0 | |
| with: | |
| working-directory: "compiler" | |
| ignore-cache: true | |
| - name: "Compiler tests" | |
| working-directory: "compiler" | |
| run: "vendor/bin/phpunit -c tests/phpunit.xml tests" | |
| - name: "Compiler PHPStan" | |
| working-directory: "compiler" | |
| run: "vendor/bin/phpstan analyse -l 8 src tests" | |
| - name: "Prepare for PHAR compilation" | |
| working-directory: "compiler" | |
| run: "php bin/prepare" | |
| - name: "Dump autoloader one more time for attributes" | |
| run: "composer dump" | |
| - name: "Install Box dependencies" | |
| uses: "ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda" # v4.0.0 | |
| with: | |
| working-directory: "compiler/box" | |
| ignore-cache: true | |
| - name: "Compile PHAR" | |
| working-directory: "compiler/build" | |
| run: "php ../box/vendor/bin/box compile --no-parallel --sort-compiled-files" | |
| # Box leaves the members with mtime 0 here, which OPcache refuses to | |
| # cache at all; the commit date also keeps consecutive builds apart for | |
| # opcache.validate_timestamps (see TurboProcessRestarter::resolveOpcacheArgs()). | |
| # resign.php fails if any member is left at mtime 0. | |
| - name: "Stamp PHAR member timestamps" | |
| run: php compiler/build/resign.php tmp/phpstan.phar "$(git log -1 --format=%cI)" | |
| - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: phar-file | |
| path: tmp/phpstan.phar | |
| - name: "Run PHAR" | |
| working-directory: "compiler" | |
| run: "../tmp/phpstan.phar list" | |
| - name: "Delete PHAR" | |
| run: "rm tmp/phpstan.phar" | |
| - name: "Set autoloader suffix" | |
| run: "composer config autoloader-suffix PHPStanChecksum" | |
| - uses: "ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda" # v4.0.0 | |
| env: | |
| COMPOSER_ROOT_VERSION: "2.3.x-dev" | |
| with: | |
| ignore-cache: true | |
| - name: "Compile PHAR for checksum" | |
| working-directory: "compiler/build" | |
| run: "php ../box/vendor/bin/box compile --no-parallel --sort-compiled-files" | |
| env: | |
| PHAR_CHECKSUM: "1" | |
| COMPOSER_ROOT_VERSION: "2.3.x-dev" | |
| - name: "Re-sign PHAR" | |
| run: "php compiler/build/resign.php tmp/phpstan.phar" | |
| - name: "Unset autoloader suffix" | |
| run: "composer config autoloader-suffix --unset" | |
| - name: "Save checksum" | |
| id: "checksum" | |
| run: echo "md5=$(md5sum tmp/phpstan.phar | cut -d' ' -f1)" >> "$GITHUB_OUTPUT" | |
| - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: phar-file-checksum | |
| path: tmp/phpstan.phar | |
| - name: "Delete checksum PHAR" | |
| run: "rm tmp/phpstan.phar" | |
| - uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2 | |
| id: changes | |
| with: | |
| filters: | | |
| compiler: | |
| - 'compiler/**' | |
| - '.github/workflows/phar.yml' | |
| - '.github/scripts/**' | |
| turbo-version: | |
| name: "Turbo Extension Version Check" | |
| runs-on: "ubuntu-latest" | |
| timeout-minutes: 5 | |
| steps: | |
| - name: Harden the runner (Audit all outbound calls) | |
| uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 | |
| with: | |
| egress-policy: audit | |
| - name: "Checkout" | |
| uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 | |
| with: | |
| persist-credentials: false | |
| fetch-depth: 0 # git log over turbo-ext/src needs full history | |
| - name: "Install PHP" | |
| uses: "shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240" # v2.37.2 | |
| with: | |
| coverage: "none" | |
| php-version: "8.5" | |
| # side-by-side.php collects the attributes with reflection against the | |
| # dumped autoloader and byte-compares the generated vendor/turbo-* files | |
| - uses: "ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda" # v4.0.0 | |
| with: | |
| ignore-cache: true | |
| - name: "Check the shadowed PHP and C++ implementations are in sync (method parity)" | |
| run: "php turbo-ext/bin/side-by-side.php" | |
| - name: "Check the vendored php-parser matches the version the native parser engine was ported against" | |
| run: | | |
| INSTALLED="$(jq -r '.packages[] | select(.name == "nikic/php-parser") | .version' composer.lock)" | |
| echo "composer.lock: $INSTALLED, native engine ported against: $SUPPORTED_PHP_PARSER_VERSION" | |
| if [ "$INSTALLED" != "$SUPPORTED_PHP_PARSER_VERSION" ]; then | |
| echo "::error::nikic/php-parser was updated to $INSTALLED but turbo-ext/src/parser/ was ported against $SUPPORTED_PHP_PARSER_VERSION." | |
| echo "::error::Update procedure: diff ParserAbstract.php helpers between the two versions (grammar tables need nothing — they are read at run time), run turbo-ext/bin/generate-parser-actions.php and port any flagged closure bodies via action-overrides/, run turbo-ext/tests/parser-corpus.php and turbo-ext/tests/parser-upstream-corpus.php until byte-identical, run the full suite, then bump SUPPORTED_PHP_PARSER_VERSION here and the extension version pin." | |
| exit 1 | |
| fi | |
| - name: "Check the parser actions are generated (regenerate and diff)" | |
| run: | | |
| php turbo-ext/bin/generate-parser-actions.php | |
| git diff --exit-code turbo-ext/src/parser/ | |
| - name: "Check TurboExtensionEnabler::EXPECTED_EXTENSION_VERSION against the last commit touching turbo-ext/src" | |
| run: | | |
| EXPECTED_SHA="$(git log -1 --format=%H -- turbo-ext/src | cut -c1-7)" | |
| ENABLER="$(sed -n "s/.*EXPECTED_EXTENSION_VERSION = '\([^']*\)'.*/\1/p" src/Turbo/TurboExtensionEnabler.php)" | |
| echo "last commit touching turbo-ext/src: $EXPECTED_SHA" | |
| echo "TurboExtensionEnabler::EXPECTED_EXTENSION_VERSION: $ENABLER" | |
| if [ "$ENABLER" != "$EXPECTED_SHA" ]; then | |
| echo "::error::TurboExtensionEnabler::EXPECTED_EXTENSION_VERSION must be the short SHA of the last commit touching turbo-ext/src/." | |
| echo "::error::After changing the native side, verify the implementations still match and add a follow-up commit setting the constant to that commit's short SHA (the binary bakes its version from git at build time). Compute it only after the change lands on the target branch — a pull request commit gets a new SHA when rebased." | |
| exit 1 | |
| fi | |
| # VERSION.txt is generated into the phpstan/turbo-ext subsplit by | |
| # subsplit-turbo-ext.yml; a copy committed here would go stale and | |
| # silently win over git in phpize builds. | |
| if [ -e turbo-ext/VERSION.txt ]; then | |
| echo "::error::turbo-ext/VERSION.txt must not exist in the monorepo — it is generated per replayed commit by the subsplit-turbo-ext.yml workflow." | |
| exit 1 | |
| fi | |
| turbo-phpize: | |
| # Builds through phpize && ./configure && make — the pipeline PIE drives | |
| # when it falls back to a source build of the phpstan/turbo package | |
| # (config.m4). The distributed binaries keep coming from the Makefile | |
| # legs in turbo-compile: the libtool link here cannot statically fold | |
| # libstdc++/libgcc into the .so, which the shipped Linux binaries need | |
| # to stay independent of the host's GLIBCXX symbol versions. One PHP | |
| # version suffices — per-version compile coverage comes from | |
| # turbo-compile; this job guards the build system itself. | |
| name: "Turbo Extension phpize Build" | |
| runs-on: ${{ matrix.operating-system }} | |
| timeout-minutes: 30 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| operating-system: ["ubuntu-latest", "macos-latest"] | |
| steps: | |
| - name: Harden the runner (Audit all outbound calls) | |
| uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 | |
| with: | |
| egress-policy: audit | |
| - name: "Checkout" | |
| uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 | |
| with: | |
| persist-credentials: false | |
| fetch-depth: 0 # the VERSION.txt written below bakes from git log over turbo-ext/src | |
| - name: "Install PHP" | |
| uses: "shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240" # v2.37.2 | |
| with: | |
| coverage: "none" | |
| php-version: "8.5" | |
| - name: "Build via phpize outside the checkout" | |
| # The copy is deliberately outside the git checkout and gets a | |
| # VERSION.txt generated the same way subsplit-turbo-ext.yml commits | |
| # it into phpstan/turbo-ext — the exact shape a PIE source build | |
| # sees: no monorepo history, the version supplied by the file. | |
| run: | | |
| if [ "$RUNNER_OS" = "macOS" ]; then | |
| command -v autoconf > /dev/null || brew install autoconf | |
| fi | |
| cp -R turbo-ext "$RUNNER_TEMP/turbo-ext" | |
| git log -1 --format=%H -- turbo-ext/src | cut -c1-7 > "$RUNNER_TEMP/turbo-ext/VERSION.txt" | |
| echo "VERSION.txt: $(cat "$RUNNER_TEMP/turbo-ext/VERSION.txt")" | |
| cd "$RUNNER_TEMP/turbo-ext" | |
| phpize | |
| ./configure | |
| make -j"$(getconf _NPROCESSORS_ONLN)" | |
| - name: "Verify the built extension reports the expected version" | |
| run: | | |
| REPORTED="$(php -d extension="$RUNNER_TEMP/turbo-ext/modules/phpstan_turbo.so" -r 'echo phpversion("phpstan_turbo");')" | |
| EXPECTED="$(sed -n "s/.*EXPECTED_EXTENSION_VERSION = '\([^']*\)'.*/\1/p" src/Turbo/TurboExtensionEnabler.php)" | |
| echo "built extension reports: $REPORTED, enabler expects: $EXPECTED" | |
| [ "$REPORTED" = "$EXPECTED" ] | |
| - uses: "ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda" # v4.0.0 | |
| with: | |
| ignore-cache: true | |
| - name: "Smoke test (differential: native vs PHP implementations)" | |
| # the Type-family child processes (tests/type-family.php) locate the | |
| # extension through TURBO_DLL — its default, turbo-ext/phpstan_turbo.so, | |
| # only exists in Makefile builds | |
| env: | |
| TURBO_DLL: ${{ runner.temp }}/turbo-ext/modules/phpstan_turbo.so | |
| run: php -d extension="$RUNNER_TEMP/turbo-ext/modules/phpstan_turbo.so" turbo-ext/tests/smoke.php | |
| - name: "Arena smoke test (cross-process shared-memory records)" | |
| # the test's child processes locate the extension through TURBO_DLL — | |
| # its default, turbo-ext/phpstan_turbo.so, only exists in Makefile builds | |
| env: | |
| TURBO_DLL: ${{ runner.temp }}/turbo-ext/modules/phpstan_turbo.so | |
| run: php -d extension="$RUNNER_TEMP/turbo-ext/modules/phpstan_turbo.so" turbo-ext/tests/arena-smoke.php | |
| - name: "Forked-worker exit (exitImmediately() ends a child whose teardown would wedge)" | |
| run: php -d extension="$RUNNER_TEMP/turbo-ext/modules/phpstan_turbo.so" turbo-ext/tests/exit-immediately.php | |
| - name: "Trusted types (the optimizer pass drops type checks under the prefix only)" | |
| # the opcache.file_cache child process locates the extension through | |
| # TURBO_DLL — its default, turbo-ext/phpstan_turbo.so, only exists in | |
| # Makefile builds | |
| env: | |
| TURBO_DLL: ${{ runner.temp }}/turbo-ext/modules/phpstan_turbo.so | |
| run: php -d extension="$RUNNER_TEMP/turbo-ext/modules/phpstan_turbo.so" -d opcache.enable_cli=1 turbo-ext/tests/trusted-types.php | |
| - name: "Signature parity (reflect native classes against the PHP twins)" | |
| run: php -d extension="$RUNNER_TEMP/turbo-ext/modules/phpstan_turbo.so" turbo-ext/tests/signature-parity.php | |
| - name: "Parser corpus (differential: native vs PHP ASTs must be byte-identical)" | |
| run: php -d extension="$RUNNER_TEMP/turbo-ext/modules/phpstan_turbo.so" -d memory_limit=4G turbo-ext/tests/parser-corpus.php | |
| - name: "make install into a scratch root (PIE finishes with make install)" | |
| run: | | |
| make -C "$RUNNER_TEMP/turbo-ext" install INSTALL_ROOT="$RUNNER_TEMP/install-root" | |
| find "$RUNNER_TEMP/install-root" -name 'phpstan_turbo.so' | grep . | |
| turbo-origins: | |
| name: "Find Reusable Turbo Extension Builds" | |
| # Most pushes and pull requests leave the extension's build inputs | |
| # untouched, and compiling all of its legs takes up to 12 minutes. For | |
| # each binary, this finds the newest earlier run that compiled it from | |
| # identical build inputs (.github/scripts/find-turbo-origins.sh has the | |
| # exact conditions), and its compile leg skips the build altogether: it | |
| # runs on ubuntu-latest, outside any build container, and only carries | |
| # that binary over into this run. The binary still goes through the | |
| # version check and the differential tests of the turbo-differential | |
| # jobs, like a compiled one — they compare the native side with the PHP | |
| # side of this commit, which may have changed. A binary reused on a | |
| # PHP 8.6 leg was compiled against prerelease headers that may have | |
| # moved on since; if it no longer loads, the version check there fails | |
| # until the next change to the build inputs compiles it again. | |
| # Only runs of this workflow triggered by a push to 2.3.x qualify — the | |
| # same runs whose compiled binaries the commit job stages into | |
| # phpstan/phpstan — so reusing their artifacts trusts no code the | |
| # distributed binaries do not already depend on. actions/cache was ruled | |
| # out for that reason: any workflow running on 2.3.x can write entries | |
| # into the branch's cache, one this workflow would then restore. | |
| runs-on: "ubuntu-latest" | |
| timeout-minutes: 5 | |
| permissions: | |
| contents: read # actions/checkout of this repository | |
| actions: read # lists the earlier runs of this workflow and their artifacts | |
| outputs: | |
| origins: ${{ steps.find.outputs.origins }} | |
| steps: | |
| - name: Harden the runner (Audit all outbound calls) | |
| uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 | |
| with: | |
| egress-policy: audit | |
| - name: "Checkout" | |
| uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 | |
| with: | |
| persist-credentials: false | |
| fetch-depth: 0 # the earlier runs' commits are diffed against HEAD | |
| - name: "Find the newest build of each binary from identical build inputs" | |
| id: find | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| BRANCH: "2.3.x" | |
| run: bash .github/scripts/find-turbo-origins.sh | |
| turbo-compile: | |
| name: "Compile Turbo Extension" | |
| # A leg whose binary turbo-origins found in an earlier run builds | |
| # nothing: it runs on ubuntu-latest without a container and carries that | |
| # binary over (runs-on and container cannot read env, hence the lookup | |
| # of ORIGIN_RUN_ID repeated in both). The tests of every binary, reused | |
| # or compiled, run in the turbo-differential jobs, so the commit job, | |
| # which needs the binaries, does not wait for them. | |
| runs-on: ${{ fromJSON(needs.turbo-origins.outputs.origins || '{}')[format('{0}-{1}-php{2}{3}', matrix.target.artifact, matrix.target.name, matrix.php-version, matrix.target.suffix || '')] && 'ubuntu-latest' || matrix.target.runs-on }} | |
| # Container legs pin the built binaries' userland floor independently of | |
| # the runner image. The gnu legs run in a prebuilt ubuntu:22.04-based | |
| # image (.github/turbo-build/Dockerfile, published to GHCR by | |
| # turbo-build-image.yml) with the PHP toolchain baked in: the glibc | |
| # symbol floor stays at 2.34 (RHEL 9, Ubuntu 22.04+, Debian 12+) and the | |
| # jobs run no apt at all — ports.ubuntu.com (the only Ubuntu arm64 | |
| # mirror) has repeated outages that failed the arm64 legs, and the | |
| # from-scratch PPA setup cost ~1 minute on every x86_64 leg. alpine:3.24 | |
| # builds the musl variant; apk installs take seconds from a CDN, so no | |
| # prebuilt image is needed there. The linux-musl-arm64 leg lives in the | |
| # separate turbo-compile-musl-arm64 job below: JavaScript-based actions | |
| # cannot run in Alpine containers on arm64 runners (the runner only | |
| # ships an x64 musl Node), so that job drives the Alpine container | |
| # through docker exec instead of a `container:` leg. | |
| # Linux ZTS legs (suffix -zts) exist from PHP 8.6 on: the official Docker | |
| # images build every 8.6+ variant thread-safe, cli and alpine included | |
| # (docker-library/php#1686), while distros, ppa:ondrej/php, sury, Remi | |
| # and setup-php keep shipping an NTS CLI — so 8.6+ needs both. Up to 8.5 | |
| # thread-safe PHP on Linux (FrankenPHP, php:*-zts images) is a few | |
| # percent of hosts and gets no binary; the commit job deletes those from | |
| # phpstan/phpstan (TURBO_RETIRED_BINARIES). Windows has ZTS legs for | |
| # every minor: XAMPP, WampServer and Scoop default to TS PHP. | |
| # The gnu-php8.6 and gnu-php8.6-zts images build the official 8.6 | |
| # prerelease tarball (see the Dockerfile); the musl ZTS legs build the | |
| # same tarball in alpine:3.24, since Alpine packages no thread-safe PHP. | |
| # Alpine's NTS php86 packages are currently only in edge/testing. | |
| container: ${{ !(fromJSON(needs.turbo-origins.outputs.origins || '{}')[format('{0}-{1}-php{2}{3}', matrix.target.artifact, matrix.target.name, matrix.php-version, matrix.target.suffix || '')]) && (matrix.target.family == 'gnu' && format('ghcr.io/phpstan/turbo-build:gnu-php{0}{1}', matrix.php-version, matrix.target.suffix || '') || matrix.target.family == 'musl' && matrix.php-version == '8.6' && matrix.target.suffix != '-zts' && 'alpine:edge' || matrix.target.container) || '' }} | |
| needs: turbo-origins | |
| timeout-minutes: 30 | |
| permissions: | |
| contents: read # actions/checkout of this repository | |
| actions: read # downloads a reused binary from an earlier run, see turbo-origins | |
| env: | |
| PHP_MINOR: ${{ matrix.php-version }} | |
| PHP_ZTS: ${{ matrix.target.suffix == '-zts' && '1' || '0' }} | |
| TURBO_ARTIFACT: "${{ matrix.target.artifact }}-${{ matrix.target.name }}-php${{ matrix.php-version }}${{ matrix.target.suffix }}" | |
| ORIGIN_RUN_ID: ${{ fromJSON(needs.turbo-origins.outputs.origins || '{}')[format('{0}-{1}-php{2}{3}', matrix.target.artifact, matrix.target.name, matrix.php-version, matrix.target.suffix || '')] }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| php-version: ["8.3", "8.4", "8.5", "8.6"] | |
| target: | |
| - name: "linux-gnu-x86_64" | |
| runs-on: "ubuntu-latest" | |
| family: "gnu" | |
| artifact: "phpstan_turbo" | |
| - name: "linux-gnu-arm64" | |
| runs-on: "ubuntu-24.04-arm" | |
| family: "gnu" | |
| artifact: "phpstan_turbo" | |
| - name: "linux-musl-x86_64" | |
| runs-on: "ubuntu-latest" | |
| container: "alpine:3.24" | |
| family: "musl" | |
| artifact: "phpstan_turbo" | |
| # arm64 (Apple Silicon) only: setup-php dropped Intel macOS. An | |
| # x86_64 PHP — native Intel, or Rosetta on Apple Silicon, which | |
| # reports x86_64 too — finds no binary: TurboExtensionSelector | |
| # resolves Darwin to macos-arm64 only when php_uname('m') says arm64. | |
| - name: "macos-arm64" | |
| runs-on: "macos-latest" | |
| family: "macos" | |
| artifact: "phpstan_turbo" | |
| # thread-safe builds, 8.6+ only (see above); the musl arm64 one is in | |
| # turbo-compile-musl-arm64 | |
| include: | |
| - php-version: "8.6" | |
| target: | |
| name: "linux-gnu-x86_64" | |
| runs-on: "ubuntu-latest" | |
| family: "gnu" | |
| artifact: "phpstan_turbo" | |
| suffix: "-zts" | |
| - php-version: "8.6" | |
| target: | |
| name: "linux-gnu-arm64" | |
| runs-on: "ubuntu-24.04-arm" | |
| family: "gnu" | |
| artifact: "phpstan_turbo" | |
| suffix: "-zts" | |
| - php-version: "8.6" | |
| target: | |
| name: "linux-musl-x86_64" | |
| runs-on: "ubuntu-latest" | |
| container: "alpine:3.24" | |
| family: "musl" | |
| artifact: "phpstan_turbo" | |
| suffix: "-zts" | |
| steps: | |
| - name: Harden the runner (Audit all outbound calls) | |
| if: env.ORIGIN_RUN_ID != '' || matrix.target.family == 'macos' | |
| uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 | |
| with: | |
| egress-policy: audit | |
| # See turbo-origins. The build steps below are all skipped on this leg. | |
| - name: "Download the extension compiled from identical build inputs" | |
| if: env.ORIGIN_RUN_ID != '' | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: ${{ env.TURBO_ARTIFACT }} | |
| path: "turbo-ext" | |
| run-id: ${{ env.ORIGIN_RUN_ID }} | |
| github-token: ${{ github.token }} | |
| - name: "Record the run that compiled the extension" | |
| if: env.ORIGIN_RUN_ID != '' | |
| run: echo "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$ORIGIN_RUN_ID" | tee turbo-reused.txt | |
| # actions/checkout needs git inside the Alpine container; PHP itself is | |
| # installed after checkout, by a script from the repository. The GNU | |
| # container legs have the toolchain baked in. | |
| - name: "Install git (Alpine container)" | |
| if: env.ORIGIN_RUN_ID == '' && matrix.target.family == 'musl' | |
| run: apk add --no-cache git | |
| - name: "Checkout" | |
| if: env.ORIGIN_RUN_ID == '' | |
| uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 | |
| with: | |
| persist-credentials: false | |
| # The Makefile bakes the extension version via git log over the | |
| # watched paths; a shallow clone would resolve every build to the | |
| # checked-out commit itself — on pull requests the synthetic merge | |
| # commit, which matches nothing. | |
| fetch-depth: 0 | |
| # In container legs the workspace volume is owned by the host runner | |
| # user while steps run as the container user, so git refuses the repo | |
| # ("dubious ownership") — which would silently bake the version as | |
| # "dev" via the Makefile's $(shell git log ...) fallback. | |
| - name: "Trust the checkout despite the container/host uid mismatch" | |
| if: env.ORIGIN_RUN_ID == '' | |
| run: git config --global --add safe.directory "$GITHUB_WORKSPACE" | |
| # The build needs php/php-config on PATH for the matrix PHP version. | |
| - name: "Install PHP and build tools (Alpine container)" | |
| if: env.ORIGIN_RUN_ID == '' && matrix.target.family == 'musl' | |
| run: sh .github/scripts/install-alpine-php.sh | |
| - name: "Install PHP (macOS)" | |
| if: env.ORIGIN_RUN_ID == '' && matrix.target.family == 'macos' | |
| uses: "shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240" # v2.37.2 | |
| with: | |
| coverage: "none" | |
| php-version: "${{ matrix.php-version }}" | |
| # The training run of `make pgo` (bin/pgo-train.sh) runs bin/phpstan on | |
| # the checkout, so the dependencies must be in place before the build. | |
| - uses: "ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda" # v4.0.0 | |
| if: env.ORIGIN_RUN_ID == '' && matrix.target.family == 'macos' | |
| with: | |
| ignore-cache: true | |
| - name: "Install Composer dependencies (pinned composer)" | |
| if: env.ORIGIN_RUN_ID == '' && matrix.target.family != 'macos' | |
| # checksum-pinned composer, run under the matrix PHP so platform | |
| # requirements are validated against the interpreter the build | |
| # trains on | |
| run: | | |
| curl -fsSLo composer.phar "https://getcomposer.org/download/$COMPOSER_VERSION/composer.phar" | |
| echo "$COMPOSER_PHAR_SHA256 composer.phar" | sha256sum -c - | |
| php composer.phar install --no-interaction --no-progress | |
| rm composer.phar | |
| - name: "Compile phpstan_turbo with strict warnings (profile-guided)" | |
| if: env.ORIGIN_RUN_ID == '' | |
| working-directory: "turbo-ext" | |
| # `make pgo`: an instrumented build, a training run of PHPStan over | |
| # its own sources with that build loaded, then the final build using | |
| # the recorded profile — the same three stages `make pgo` runs | |
| # locally, so a profile-guided binary can be reproduced outside CI. | |
| # The strict set itself lives in turbo-ext/Makefile | |
| # (STRICT_WARN_FLAGS), which documents why each exemption is there — | |
| # every one is a third-party macro expansion, not our code. Reading | |
| # it here keeps one definition instead of three copies that drift. | |
| run: | | |
| make pgo WARN_FLAGS="$(make -s print-warn-flags)" -j"$(getconf _NPROCESSORS_ONLN 2>/dev/null || nproc)" | |
| # Before signing (stripping invalidates a signature) and before the | |
| # upload, so the turbo-differential jobs test the exact binary that | |
| # ships. The symbol tables are a quarter of the .so; see `strip` | |
| # in the Makefile. | |
| - name: "Strip symbol tables" | |
| if: env.ORIGIN_RUN_ID == '' | |
| working-directory: "turbo-ext" | |
| run: | | |
| ls -l phpstan_turbo.so | |
| make strip | |
| ls -l phpstan_turbo.so | |
| # On arm64 AMFI refuses to map unsigned code, so the binary must carry | |
| # at least an ad-hoc signature. The linker applies one itself, but | |
| # re-sign deliberately instead of relying on that. Quarantine (and thus | |
| # Gatekeeper/notarization) only applies to browser downloads — | |
| # composer/git/curl installs never see it — so an ad-hoc signature is | |
| # enough and needs no secrets. | |
| - name: "Ad-hoc sign the extension (macOS)" | |
| if: env.ORIGIN_RUN_ID == '' && matrix.target.family == 'macos' | |
| working-directory: "turbo-ext" | |
| run: | | |
| codesign --force --sign - phpstan_turbo.so | |
| codesign --verify --verbose=2 phpstan_turbo.so | |
| # Stays here rather than in turbo-differential: the commit job stages | |
| # this binary, and one baked with the wrong version would ship | |
| # inactive. A reused binary passed this check in the run compiling it, | |
| # and find-turbo-origins.sh only picks runs whose last commit touching | |
| # turbo-ext/src, which the version is baked from, matches HEAD's. | |
| - name: "Verify the built extension reports the expected version" | |
| if: env.ORIGIN_RUN_ID == '' | |
| run: | | |
| REPORTED="$(php -d extension="$PWD/turbo-ext/phpstan_turbo.so" -r 'echo phpversion("phpstan_turbo");')" | |
| EXPECTED="$(sed -n "s/.*EXPECTED_EXTENSION_VERSION = '\([^']*\)'.*/\1/p" src/Turbo/TurboExtensionEnabler.php)" | |
| echo "built extension reports: $REPORTED, enabler expects: $EXPECTED" | |
| [ "$REPORTED" = "$EXPECTED" ] | |
| # the loader rejects a ts-mismatched binary, so php loading it above | |
| # proves the binary matches the interpreter; assert the interpreter | |
| # itself so a -zts artifact cannot silently carry an NTS build | |
| [ "$(php -r 'echo (int) ((bool) PHP_ZTS);')" = "$PHP_ZTS" ] | |
| # A reused binary is uploaded again under the same name, so the jobs | |
| # downstream find every binary in this run. | |
| - name: "Upload extension artifact" | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: ${{ env.TURBO_ARTIFACT }} | |
| path: "turbo-ext/phpstan_turbo.so" | |
| if-no-files-found: "error" | |
| # Keeps later runs from reusing the copy above: they take the binary | |
| # from the run that compiled it (see find-turbo-origins.sh). | |
| - name: "Mark the extension artifact as reused" | |
| if: env.ORIGIN_RUN_ID != '' | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: "turbo-reused-${{ env.TURBO_ARTIFACT }}" | |
| path: "turbo-reused.txt" | |
| if-no-files-found: "error" | |
| turbo-compile-musl-arm64: | |
| name: "Compile Turbo Extension (musl arm64)" | |
| # The arm64 counterpart of the linux-musl-x86_64 leg in turbo-compile. | |
| # It cannot be a `container: alpine` leg there: JavaScript-based actions | |
| # cannot run in Alpine containers on arm64 runners (the runner only | |
| # ships an x64 musl Node). Instead the workflow steps run on the arm64 | |
| # host and only the build commands run inside a long-lived Alpine | |
| # container via docker exec — native arm64, no QEMU. The steps mirror | |
| # the musl legs of turbo-compile one for one, including the reuse of a | |
| # binary compiled in an earlier run, whose leg runs on ubuntu-latest. | |
| runs-on: ${{ fromJSON(needs.turbo-origins.outputs.origins || '{}')[format('phpstan_turbo-linux-musl-arm64-php{0}{1}', matrix.php-version, matrix.ts == 'zts' && '-zts' || '')] && 'ubuntu-latest' || 'ubuntu-24.04-arm' }} | |
| needs: turbo-origins | |
| timeout-minutes: 30 | |
| permissions: | |
| contents: read # actions/checkout of this repository | |
| actions: read # downloads a reused binary from an earlier run, see turbo-origins | |
| env: | |
| PHP_MINOR: ${{ matrix.php-version }} | |
| PHP_ZTS: ${{ matrix.ts == 'zts' && '1' || '0' }} | |
| TURBO_ARTIFACT: "phpstan_turbo-linux-musl-arm64-php${{ matrix.php-version }}${{ matrix.ts == 'zts' && '-zts' || '' }}" | |
| ORIGIN_RUN_ID: ${{ fromJSON(needs.turbo-origins.outputs.origins || '{}')[format('phpstan_turbo-linux-musl-arm64-php{0}{1}', matrix.php-version, matrix.ts == 'zts' && '-zts' || '')] }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| php-version: ["8.3", "8.4", "8.5", "8.6"] | |
| ts: ["nts"] | |
| # thread-safe from 8.6 on, see turbo-compile | |
| include: | |
| - php-version: "8.6" | |
| ts: "zts" | |
| steps: | |
| - name: Harden the runner (Audit all outbound calls) | |
| uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 | |
| with: | |
| egress-policy: audit | |
| # See the same two steps in turbo-compile. | |
| - name: "Download the extension compiled from identical build inputs" | |
| if: env.ORIGIN_RUN_ID != '' | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: ${{ env.TURBO_ARTIFACT }} | |
| path: "turbo-ext" | |
| run-id: ${{ env.ORIGIN_RUN_ID }} | |
| github-token: ${{ github.token }} | |
| - name: "Record the run that compiled the extension" | |
| if: env.ORIGIN_RUN_ID != '' | |
| run: echo "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$ORIGIN_RUN_ID" | tee turbo-reused.txt | |
| - name: "Checkout" | |
| if: env.ORIGIN_RUN_ID == '' | |
| uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 | |
| with: | |
| persist-credentials: false | |
| # The Makefile bakes the extension version via git log over the | |
| # watched paths; a shallow clone would resolve every build to the | |
| # checked-out commit itself — on pull requests the synthetic merge | |
| # commit, which matches nothing. | |
| fetch-depth: 0 | |
| - name: "Start the Alpine build container" | |
| if: env.ORIGIN_RUN_ID == '' | |
| env: | |
| ALPINE_IMAGE: ${{ matrix.php-version == '8.6' && matrix.ts == 'nts' && 'alpine:edge' || 'alpine:3.24' }} | |
| run: | | |
| docker run -d --name alpine-build \ | |
| -v "$PWD:/work" -w /work \ | |
| -e PHP_MINOR -e PHP_ZTS -e COMPOSER_VERSION -e COMPOSER_PHAR_SHA256 \ | |
| "$ALPINE_IMAGE" sleep 7200 | |
| - name: "Install PHP and build tools" | |
| if: env.ORIGIN_RUN_ID == '' | |
| run: docker exec alpine-build sh .github/scripts/install-alpine-php.sh | |
| # The workspace volume is owned by the host runner user while the | |
| # container runs as root, so git refuses the repo ("dubious | |
| # ownership") — which would silently bake the version as "dev" via | |
| # the Makefile's $(shell git log ...) fallback. | |
| - name: "Trust the checkout despite the container/host uid mismatch" | |
| if: env.ORIGIN_RUN_ID == '' | |
| run: docker exec alpine-build git config --global --add safe.directory /work | |
| # The training run of `make pgo` runs bin/phpstan on the checkout, so | |
| # the dependencies must be in place before the build. | |
| - name: "Install Composer dependencies (pinned composer)" | |
| if: env.ORIGIN_RUN_ID == '' | |
| run: | | |
| docker exec -i alpine-build sh -e <<'EOF' | |
| curl -fsSLo composer.phar "https://getcomposer.org/download/$COMPOSER_VERSION/composer.phar" | |
| echo "$COMPOSER_PHAR_SHA256 composer.phar" | sha256sum -c - | |
| COMPOSER_HOME="$(mktemp -d)" php composer.phar install --no-interaction --no-progress | |
| rm composer.phar | |
| EOF | |
| - name: "Compile phpstan_turbo with strict warnings (profile-guided)" | |
| if: env.ORIGIN_RUN_ID == '' | |
| run: | | |
| docker exec -i -w /work/turbo-ext alpine-build sh -e <<'EOF' | |
| make pgo WARN_FLAGS="$(make -s print-warn-flags)" -j"$(getconf _NPROCESSORS_ONLN 2>/dev/null || nproc)" | |
| EOF | |
| # before the upload, so turbo-differential-musl tests the exact binary | |
| # that ships | |
| - name: "Strip symbol tables" | |
| if: env.ORIGIN_RUN_ID == '' | |
| run: | | |
| docker exec -i -w /work/turbo-ext alpine-build sh -e <<'EOF' | |
| ls -l phpstan_turbo.so | |
| make strip | |
| ls -l phpstan_turbo.so | |
| EOF | |
| - name: "Verify the built extension reports the expected version" | |
| if: env.ORIGIN_RUN_ID == '' | |
| run: | | |
| docker exec -i alpine-build sh -e <<'EOF' | |
| REPORTED="$(php -d extension="$PWD/turbo-ext/phpstan_turbo.so" -r 'echo phpversion("phpstan_turbo");')" | |
| EXPECTED="$(sed -n "s/.*EXPECTED_EXTENSION_VERSION = '\([^']*\)'.*/\1/p" src/Turbo/TurboExtensionEnabler.php)" | |
| echo "built extension reports: $REPORTED, enabler expects: $EXPECTED" | |
| [ "$REPORTED" = "$EXPECTED" ] | |
| [ "$(php -r 'echo (int) ((bool) PHP_ZTS);')" = "$PHP_ZTS" ] | |
| EOF | |
| - name: "Upload extension artifact" | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: ${{ env.TURBO_ARTIFACT }} | |
| path: "turbo-ext/phpstan_turbo.so" | |
| if-no-files-found: "error" | |
| - name: "Mark the extension artifact as reused" | |
| if: env.ORIGIN_RUN_ID != '' | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: "turbo-reused-${{ env.TURBO_ARTIFACT }}" | |
| path: "turbo-reused.txt" | |
| if-no-files-found: "error" | |
| turbo-docker-run: | |
| name: "Run with Turbo Extension (Docker PHP builds)" | |
| needs: "turbo-compile" | |
| # The docker-library php images build the official php.net tarballs | |
| # without regenerating the parser, so their runtime can differ from the | |
| # distro/setup-php builds the .so artifacts are compiled against and | |
| # that turbo-run exercises. The canonical incident is the T_* token | |
| # numbering (https://github.com/phpstan/phpstan/issues/15037): the | |
| # userland token ids are assigned by the bison that generated the | |
| # tarball's zend_language_parser.c — the 8.3.21 tarball ships the | |
| # Bison 3.8.2 numbering, the 8.3.22 one the Bison 3.0.4 numbering — | |
| # and with ids baked in at compile time the extension silently dropped | |
| # every comment, and with them every PHPDoc, on the other numbering's | |
| # builds. The next incompatibility of that kind will not announce | |
| # itself in a probe written for the last one, so this job loads the | |
| # freshly built extension into docker-library images and runs the same | |
| # make tests + make phpstan as turbo-run: any turbo-vs-environment | |
| # mismatch fails ordinary tests (verified against the pre-fix | |
| # token-numbering bug: 2462 of 21065 tests fail and self-analysis | |
| # crashes on the Bison 3.0.4 image, both green on the matched one). | |
| # The 8.3 images are pinned to one release of each known numbering; | |
| # 8.4/8.5 float on the minor tag and 8.6 on the prerelease tag so a future | |
| # release with a surprising build is picked up on the next run without | |
| # a workflow change. Legs run on x86_64 only: the token numbering comes | |
| # from the bison that generated the tarball's parser, not from arch or | |
| # libc, and per-arch coverage stays with turbo-run and | |
| # turbo-differential. The 8.6+ images are thread-safe | |
| # (docker-library/php#1686), so those legs load the -zts binaries, and | |
| # an Alpine leg covers the musl one — php:*-alpine is the other image | |
| # family people run PHPStan in. No macOS/Windows legs: those runners | |
| # cannot run Linux containers, and no alternative-numbered official | |
| # build is distributed for either platform today. | |
| runs-on: "ubuntu-latest" | |
| timeout-minutes: 60 | |
| env: | |
| IMAGE: ${{ matrix.php-version == '8.6' && format('phpstan-turbo-test:{0}', matrix.binary) || matrix.image }} | |
| permissions: | |
| contents: read | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| image: | |
| - "php:8.3.21-cli-bookworm" | |
| - "php:8.3.22-cli-bookworm" | |
| - "php:8.4-cli-bookworm" | |
| - "php:8.5-cli-bookworm" | |
| - "php:8.6-rc-cli-bookworm" | |
| - "php:8.6-rc-cli-alpine" | |
| check: ["tests", "phpstan"] | |
| include: | |
| - image: "php:8.3.21-cli-bookworm" | |
| php-version: "8.3" | |
| binary: "linux-gnu-x86_64-php8.3" | |
| - image: "php:8.3.22-cli-bookworm" | |
| php-version: "8.3" | |
| binary: "linux-gnu-x86_64-php8.3" | |
| - image: "php:8.4-cli-bookworm" | |
| php-version: "8.4" | |
| binary: "linux-gnu-x86_64-php8.4" | |
| - image: "php:8.5-cli-bookworm" | |
| php-version: "8.5" | |
| binary: "linux-gnu-x86_64-php8.5" | |
| - image: "php:8.6-rc-cli-bookworm" | |
| php-version: "8.6" | |
| binary: "linux-gnu-x86_64-php8.6-zts" | |
| - image: "php:8.6-rc-cli-alpine" | |
| php-version: "8.6" | |
| binary: "linux-musl-x86_64-php8.6-zts" | |
| # The tests legs invoke paratest directly instead of a literal | |
| # `make tests`: its install-paratest step needs a composer, which | |
| # the images do not carry — tests/vendor is installed on the | |
| # runner host below, the same way tests.yml runs the suite. | |
| - check: "tests" | |
| script: "php tests/vendor/bin/paratest --runner WrapperRunner --no-coverage" | |
| - check: "phpstan" | |
| script: "make phpstan" | |
| steps: | |
| - name: Harden the runner (Audit all outbound calls) | |
| uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 | |
| with: | |
| egress-policy: audit | |
| - name: "Checkout" | |
| uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 | |
| with: | |
| persist-credentials: false | |
| # vendor/ (and tests/vendor for the tests legs) is installed on the | |
| # runner host and only read from the mounted workspace: the | |
| # docker-library images carry neither a composer nor an unzip. | |
| - name: "Install PHP" | |
| uses: "shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240" # v2.37.2 | |
| with: | |
| coverage: "none" | |
| php-version: "${{ matrix.php-version }}" | |
| - uses: "ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda" # v4.0.0 | |
| with: | |
| ignore-cache: true | |
| - name: "Install paratest" | |
| if: matrix.check == 'tests' | |
| run: composer install --working-dir tests | |
| - name: "Download extension artifact" | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: "phpstan_turbo-${{ matrix.binary }}" | |
| path: "turbo-ext" | |
| - name: "Add mbstring to the PHP 8.6 prerelease image" | |
| if: matrix.php-version == '8.6' | |
| env: | |
| PHP_IMAGE: ${{ matrix.image }} | |
| # Unlike the stable images, the 8.6 prerelease omits mbstring, | |
| # which PHPUnit and PHPStan's test fixtures require. The Alpine | |
| # images also drop the build tools after building PHP, and carry no | |
| # bash, which the run step below needs, and no make, which | |
| # `make phpstan` needs. | |
| run: | | |
| docker build --build-arg "PHP_IMAGE=$PHP_IMAGE" -t "$IMAGE" - <<'DOCKERFILE' | |
| ARG PHP_IMAGE | |
| FROM ${PHP_IMAGE} | |
| RUN if command -v apk > /dev/null; then \ | |
| apk add --no-cache bash make oniguruma \ | |
| && apk add --no-cache --virtual .mbstring-build-deps $PHPIZE_DEPS oniguruma-dev \ | |
| && docker-php-ext-install mbstring \ | |
| && apk del .mbstring-build-deps; \ | |
| else \ | |
| apt-get update && apt-get install -y --no-install-recommends libonig-dev \ | |
| && docker-php-ext-install mbstring \ | |
| && rm -rf /var/lib/apt/lists/*; \ | |
| fi | |
| DOCKERFILE | |
| # Guards the matrix above: a binary of the wrong thread-safety would | |
| # fail the probe below anyway, but with an undefined-symbol error | |
| # instead of this message. | |
| - name: "Verify the binary matches the image's thread-safety" | |
| env: | |
| BINARY: ${{ matrix.binary }} | |
| run: | | |
| IMAGE_ZTS="$(docker run --rm "$IMAGE" php -r 'echo (int) PHP_ZTS;')" | |
| BINARY_ZTS=$([ "${BINARY%-zts}" != "$BINARY" ] && echo 1 || echo 0) | |
| echo "image PHP_ZTS=$IMAGE_ZTS, binary $BINARY" | |
| [ "$IMAGE_ZTS" = "$BINARY_ZTS" ] | |
| # Fast fail with a one-line diagnosis: a token-numbering mismatch | |
| # would otherwise surface as thousands of unrelated-looking failures | |
| # in the suite run below. | |
| - name: "Token-id probe in ${{ matrix.image }}" | |
| run: docker run --rm -v "$PWD:/work" -w /work "$IMAGE" php -d extension=/work/turbo-ext/phpstan_turbo.so turbo-ext/tests/token-id-probe.php | |
| # Bind-mounted into conf.d of every docker run below. The images | |
| # ship no php.ini, so memory_limit would default to 128M, which the | |
| # test bootstrap (DI container compilation) already exceeds. | |
| - name: "Prepare the extension ini" | |
| run: printf 'extension=/work/turbo-ext/phpstan_turbo.so\nmemory_limit=-1\n' > "$RUNNER_TEMP/turbo-docker.ini" | |
| - name: "Verify the extension is active in ${{ matrix.image }}" | |
| run: | | |
| docker run --rm -v "$PWD:/work" -v "$RUNNER_TEMP/turbo-docker.ini:/usr/local/etc/php/conf.d/zz-turbo.ini" -w /work "$IMAGE" php -r ' | |
| require "vendor/autoload.php"; | |
| PHPStan\Turbo\TurboExtensionEnabler::activateIfCompatible(); | |
| if (!PHPStan\Turbo\TurboExtensionEnabler::isActive() || !(new ReflectionMethod("PHPStan\\Analyser\\ScopeOps", "nodeKey"))->isInternal()) { | |
| fwrite(STDERR, "turbo extension is not shadowing ScopeOps" . PHP_EOL); | |
| exit(1); | |
| } | |
| ' | |
| - name: "Run ${{ matrix.check }} in ${{ matrix.image }}" | |
| # --shm-size: the arena allocates cross-worker shared memory, and | |
| # docker's default 64M /dev/shm makes parallel analysis SIGBUS. | |
| env: | |
| SCRIPT: ${{ matrix.script }} | |
| run: docker run --rm --shm-size=1g -v "$PWD:/work" -v "$RUNNER_TEMP/turbo-docker.ini:/usr/local/etc/php/conf.d/zz-turbo.ini" -w /work "$IMAGE" bash -c "$SCRIPT" | |
| turbo-compile-windows: | |
| name: "Compile Turbo Extension (Windows)" | |
| # windows-latest and windows-2025 serve the VS2026 image, whose 14.5x | |
| # toolset links DLLs the official php.net binaries (built with vs17, | |
| # toolset 14.4x) refuse to load — PHP's loader rejects modules linked | |
| # with a newer toolset generation than the core. The windows-2022 image | |
| # is the one that ships VS2022. | |
| # PHP 8.6's vs18 development packs need the VS2026 toolchain. | |
| # A leg reusing a binary (see turbo-compile) runs on ubuntu-latest. | |
| runs-on: ${{ fromJSON(needs.turbo-origins.outputs.origins || '{}')[format('phpstan_turbo-windows-x86_64-php{0}{1}', matrix.php-version, matrix.ts == 'zts' && '-zts' || '')] && 'ubuntu-latest' || (matrix.php-version == '8.6' && 'windows-2025-vs2026' || 'windows-2022') }} | |
| needs: turbo-origins | |
| timeout-minutes: 40 | |
| permissions: | |
| contents: read # actions/checkout of this repository | |
| actions: read # downloads a reused binary from an earlier run, see turbo-origins | |
| env: | |
| # php/php-sdk-binary-tools — the toolchain every PHP Windows build | |
| # uses (ships bison and the unix tools phpize/configure expect). | |
| PHP_SDK_COMMIT: "c73faaf1cce914e2fc04da5587132f8f425996af" # php-sdk-2.7.1 | |
| TURBO_ARTIFACT: "phpstan_turbo-windows-x86_64-php${{ matrix.php-version }}${{ matrix.ts == 'zts' && '-zts' || '' }}" | |
| ORIGIN_RUN_ID: ${{ fromJSON(needs.turbo-origins.outputs.origins || '{}')[format('phpstan_turbo-windows-x86_64-php{0}{1}', matrix.php-version, matrix.ts == 'zts' && '-zts' || '')] }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| php-version: ["8.3", "8.4", "8.5", "8.6"] | |
| ts: ["nts", "zts"] | |
| # The official 8.3 Windows binaries are built with VS16 (linker | |
| # 14.2x) and PHP's module loader is one-directional (module toolset | |
| # generation <= core's), so the 8.3 DLL must be linked with the v142 | |
| # toolset — which the windows-2022 image ships alongside the default | |
| # v143. The devel pack name carries the same vs infix. | |
| include: | |
| - php-version: "8.3" | |
| vs: "vs16" | |
| toolset: "14.29" | |
| - php-version: "8.4" | |
| vs: "vs17" | |
| - php-version: "8.5" | |
| vs: "vs17" | |
| - php-version: "8.6" | |
| vs: "vs18" | |
| steps: | |
| # See the same two steps in turbo-compile; this leg runs on | |
| # ubuntu-latest. | |
| - name: "Download the extension compiled from identical build inputs" | |
| if: env.ORIGIN_RUN_ID != '' | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: ${{ env.TURBO_ARTIFACT }} | |
| path: "turbo-ext" | |
| run-id: ${{ env.ORIGIN_RUN_ID }} | |
| github-token: ${{ github.token }} | |
| - name: "Record the run that compiled the extension" | |
| if: env.ORIGIN_RUN_ID != '' | |
| run: | | |
| echo "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$ORIGIN_RUN_ID" | tee turbo-reused.txt | |
| echo "DLL_PATH=turbo-ext/php_phpstan_turbo.dll" >> "$GITHUB_ENV" | |
| - name: "Checkout" | |
| if: env.ORIGIN_RUN_ID == '' | |
| uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 | |
| with: | |
| persist-credentials: false | |
| # full history: the extension version is baked from git log over | |
| # the watched paths | |
| fetch-depth: 0 | |
| - name: "Install PHP" | |
| if: env.ORIGIN_RUN_ID == '' | |
| uses: "shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240" # v2.37.2 | |
| env: | |
| phpts: "${{ matrix.ts == 'zts' && 'ts' || 'nts' }}" | |
| with: | |
| coverage: "none" | |
| php-version: "${{ matrix.php-version }}" | |
| - name: "Install the matching PHP 8.6 prerelease runtime" | |
| if: env.ORIGIN_RUN_ID == '' && matrix.php-version == '8.6' | |
| shell: bash | |
| env: | |
| MATRIX_TS: ${{ matrix.ts }} | |
| run: bash .github/scripts/install-php86-windows.sh | |
| - name: "Compute the extension version and download the build tools" | |
| if: env.ORIGIN_RUN_ID == '' | |
| shell: bash | |
| env: | |
| MATRIX_TS: ${{ matrix.ts }} | |
| MATRIX_VS: ${{ matrix.vs }} | |
| run: | | |
| SHA=$(git log -1 --format=%H -- turbo-ext/src | cut -c1-7) | |
| echo "extension version: $SHA" | |
| echo "PHPSTANTURBO_VERSION=$SHA" >> "$GITHUB_ENV" | |
| FULL=$(php -r 'echo PHP_VERSION;') | |
| # thread-safe devel packs carry no infix, NTS ones carry -nts | |
| INFIX=$([ "$MATRIX_TS" = "zts" ] && echo "" || echo "-nts") | |
| PACK="php-devel-pack-$FULL$INFIX-Win32-$MATRIX_VS-x64.zip" | |
| echo "devel pack: $PACK" | |
| curl -fsSLo devel-pack.zip "https://windows.php.net/downloads/releases/$PACK" \ | |
| || curl -fsSLo devel-pack.zip "https://windows.php.net/downloads/releases/archives/$PACK" \ | |
| || curl -fsSLo devel-pack.zip "https://downloads.php.net/~windows/qa/$PACK" \ | |
| || curl -fsSLo devel-pack.zip "https://downloads.php.net/~windows/qa/archives/$PACK" | |
| unzip -q devel-pack.zip -d /c/php-devel | |
| git clone -q https://github.com/php/php-sdk-binary-tools.git /c/php-sdk | |
| git -C /c/php-sdk checkout -q "$PHP_SDK_COMMIT" | |
| - name: "Set up MSVC environment" | |
| if: env.ORIGIN_RUN_ID == '' | |
| uses: ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756 # v1.13.0 | |
| with: | |
| arch: x64 | |
| # empty on the vs17/vs18 legs = the image's default toolset | |
| toolset: ${{ matrix.toolset }} | |
| - name: "Build the extension" | |
| if: env.ORIGIN_RUN_ID == '' | |
| shell: cmd | |
| working-directory: turbo-ext | |
| run: | | |
| for /d %%d in (C:\php-devel\php-*-devel-*) do set DEVEL=%%d | |
| set PATH=%DEVEL%;C:\php-sdk\bin;C:\php-sdk\msys2\usr\bin;%PATH% | |
| call phpize.bat | |
| if errorlevel 1 exit /b 1 | |
| call configure.bat --enable-phpstan-turbo | |
| if errorlevel 1 exit /b 1 | |
| nmake | |
| # See the same step in turbo-compile. | |
| - name: "Verify the built extension reports the expected version" | |
| if: env.ORIGIN_RUN_ID == '' | |
| shell: bash | |
| env: | |
| MATRIX_TS: ${{ matrix.ts }} | |
| run: | | |
| DLL=$(find turbo-ext -name "php_phpstan_turbo.dll" | head -1) | |
| [ -n "$DLL" ] | |
| echo "DLL_PATH=$DLL" >> "$GITHUB_ENV" | |
| REPORTED="$(php -d extension="$(cygpath -w "$PWD/$DLL")" -r 'echo phpversion("phpstan_turbo");')" | |
| EXPECTED="$(sed -n "s/.*EXPECTED_EXTENSION_VERSION = '\([^']*\)'.*/\1/p" src/Turbo/TurboExtensionEnabler.php)" | |
| echo "built extension reports: $REPORTED, enabler expects: $EXPECTED" | |
| [ "$REPORTED" = "$EXPECTED" ] | |
| # the loader rejects a ts-mismatched DLL, so php loading it above | |
| # proves the DLL matches the interpreter; assert the interpreter | |
| # itself so both sides cannot silently be NTS on the zts leg | |
| WANT_ZTS=$([ "$MATRIX_TS" = "zts" ] && echo 1 || echo 0) | |
| [ "$(php -r 'echo (int) ((bool) PHP_ZTS);')" = "$WANT_ZTS" ] | |
| - name: "Upload extension artifact" | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: ${{ env.TURBO_ARTIFACT }} | |
| path: "${{ env.DLL_PATH }}" | |
| if-no-files-found: "error" | |
| - name: "Mark the extension artifact as reused" | |
| if: env.ORIGIN_RUN_ID != '' | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: "turbo-reused-${{ env.TURBO_ARTIFACT }}" | |
| path: "turbo-reused.txt" | |
| if-no-files-found: "error" | |
| turbo-artifact: | |
| name: "Aggregate Turbo Extension Artifact" | |
| needs: | |
| - turbo-compile | |
| - turbo-compile-musl-arm64 | |
| - turbo-compile-windows | |
| runs-on: "ubuntu-latest" | |
| timeout-minutes: 5 | |
| steps: | |
| - name: Harden the runner (Audit all outbound calls) | |
| uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 | |
| with: | |
| egress-policy: audit | |
| - name: "Download extension artifacts" | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| pattern: "phpstan_turbo-*" | |
| path: "turbo-artifacts" | |
| - name: "Arrange the distribution layout" | |
| # phpstan_turbo-<target>-php<minor>[-zts] → | |
| # <target>/phpstan_turbo-<minor>[-zts].<so|dll>, derived from the | |
| # artifact names so new targets need no change here. | |
| # The integration/extension/other test workflows in phpstan/phpstan | |
| # download this artifact next to the phar under test. | |
| run: | | |
| for dir in turbo-artifacts/phpstan_turbo-*; do | |
| name="${dir#turbo-artifacts/phpstan_turbo-}" | |
| target="${name%-php*}" | |
| minor="${name##*-php}" | |
| for file in "$dir"/*; do | |
| install -D -m 644 "$file" "turbo-ext-dist/$target/phpstan_turbo-$minor.${file##*.}" | |
| done | |
| done | |
| find turbo-ext-dist -type f | sort | |
| - name: "Upload aggregated artifact" | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: "turbo-ext-files" | |
| path: "turbo-ext-dist" | |
| if-no-files-found: "error" | |
| turbo-differential: | |
| name: "Run with Turbo Extension (differential tests)" | |
| # The differential tests (native vs the PHP implementations of this | |
| # commit) for every binary the compile jobs hand over, compiled or | |
| # reused, under the runtime setup-php installs on the platform the | |
| # binary is built for. Kept out of the compile jobs so the commit job, | |
| # which only needs the binaries, does not wait for them (see there). | |
| # The musl binaries are tested in turbo-differential-musl. The linux-gnu | |
| # 8.6 legs run in the image the binary is compiled in instead: setup-php's | |
| # Linux 8.6 is a nightly snapshot that can predate the release the binary | |
| # targets, and a module API mismatch refuses to load it at all. | |
| needs: | |
| - turbo-compile | |
| - turbo-compile-windows | |
| runs-on: ${{ matrix.runs-on }} | |
| container: ${{ matrix.container || '' }} | |
| timeout-minutes: 30 | |
| defaults: | |
| run: | |
| shell: bash | |
| env: | |
| TURBO_ARTIFACT: "phpstan_turbo-${{ matrix.target }}-php${{ matrix.php-version }}${{ matrix.ts == 'zts' && '-zts' || '' }}" | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| php-version: ["8.3", "8.4", "8.5", "8.6"] | |
| target: ["linux-gnu-x86_64", "linux-gnu-arm64", "macos-arm64", "windows-x86_64"] | |
| ts: ["nts", "zts"] | |
| # Windows ships ZTS binaries for every minor, Linux from 8.6 on — | |
| # added by the include entries below (see turbo-compile) | |
| exclude: | |
| - target: "linux-gnu-x86_64" | |
| ts: "zts" | |
| - target: "linux-gnu-arm64" | |
| ts: "zts" | |
| - target: "macos-arm64" | |
| ts: "zts" | |
| include: | |
| - target: "linux-gnu-x86_64" | |
| runs-on: "ubuntu-latest" | |
| - target: "linux-gnu-arm64" | |
| runs-on: "ubuntu-24.04-arm" | |
| - target: "macos-arm64" | |
| runs-on: "macos-latest" | |
| # see turbo-run for why windows-latest loads the DLL | |
| - target: "windows-x86_64" | |
| runs-on: "windows-latest" | |
| - target: "linux-gnu-x86_64" | |
| php-version: "8.6" | |
| container: "ghcr.io/phpstan/turbo-build:gnu-php8.6" | |
| - target: "linux-gnu-arm64" | |
| php-version: "8.6" | |
| container: "ghcr.io/phpstan/turbo-build:gnu-php8.6" | |
| - target: "linux-gnu-x86_64" | |
| php-version: "8.6" | |
| ts: "zts" | |
| runs-on: "ubuntu-latest" | |
| container: "ghcr.io/phpstan/turbo-build:gnu-php8.6-zts" | |
| - target: "linux-gnu-arm64" | |
| php-version: "8.6" | |
| ts: "zts" | |
| runs-on: "ubuntu-24.04-arm" | |
| container: "ghcr.io/phpstan/turbo-build:gnu-php8.6-zts" | |
| steps: | |
| - name: Harden the runner (Audit all outbound calls) | |
| if: ${{ !matrix.container }} | |
| uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 | |
| with: | |
| egress-policy: audit | |
| - name: "Checkout" | |
| uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 | |
| with: | |
| persist-credentials: false | |
| - name: "Install PHP" | |
| if: ${{ !matrix.container }} | |
| uses: "shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240" # v2.37.2 | |
| env: | |
| phpts: "${{ matrix.ts == 'zts' && 'ts' || 'nts' }}" | |
| with: | |
| coverage: "none" | |
| php-version: "${{ matrix.php-version }}" | |
| - name: "Install the matching PHP 8.6 prerelease runtime" | |
| if: matrix.target == 'windows-x86_64' && matrix.php-version == '8.6' | |
| env: | |
| MATRIX_TS: ${{ matrix.ts }} | |
| run: bash .github/scripts/install-php86-windows.sh | |
| - uses: "ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda" # v4.0.0 | |
| if: ${{ !matrix.container }} | |
| with: | |
| ignore-cache: true | |
| - name: "Install Composer dependencies (pinned composer)" | |
| if: ${{ matrix.container }} | |
| run: | | |
| curl -fsSLo composer.phar "https://getcomposer.org/download/$COMPOSER_VERSION/composer.phar" | |
| echo "$COMPOSER_PHAR_SHA256 composer.phar" | sha256sum -c - | |
| php composer.phar install --no-interaction --no-progress | |
| rm composer.phar | |
| - name: "Download extension artifact" | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: ${{ env.TURBO_ARTIFACT }} | |
| path: "turbo-ext" | |
| # The tests' child processes locate the extension through TURBO_DLL | |
| # too; its default, turbo-ext/phpstan_turbo.so, misses the DLL. | |
| - name: "Locate the extension" | |
| run: | | |
| FILE="$(find turbo-ext -maxdepth 1 \( -name phpstan_turbo.so -o -name php_phpstan_turbo.dll \) | head -1)" | |
| [ -n "$FILE" ] | |
| EXT="$PWD/$FILE" | |
| if command -v cygpath > /dev/null; then | |
| EXT="$(cygpath -w "$EXT")" | |
| fi | |
| echo "TURBO_DLL=$EXT" >> "$GITHUB_ENV" | |
| - name: "Verify the extension reports the expected version" | |
| env: | |
| MATRIX_TS: ${{ matrix.ts }} | |
| run: | | |
| REPORTED="$(php -d extension="$TURBO_DLL" -r 'echo phpversion("phpstan_turbo");')" | |
| EXPECTED="$(sed -n "s/.*EXPECTED_EXTENSION_VERSION = '\([^']*\)'.*/\1/p" src/Turbo/TurboExtensionEnabler.php)" | |
| echo "extension reports: $REPORTED, enabler expects: $EXPECTED" | |
| [ "$REPORTED" = "$EXPECTED" ] | |
| # the loader rejects a ts-mismatched binary, so php loading it above | |
| # proves the binary matches the interpreter; assert the interpreter | |
| # itself so both sides cannot silently be NTS on the zts legs | |
| WANT_ZTS=$([ "$MATRIX_TS" = "zts" ] && echo 1 || echo 0) | |
| [ "$(php -r 'echo (int) ((bool) PHP_ZTS);')" = "$WANT_ZTS" ] | |
| - name: "Smoke test (differential: native vs PHP implementations)" | |
| run: php -d extension="$TURBO_DLL" turbo-ext/tests/smoke.php | |
| # One NodeScopeResolver walk over the default corpus with the native | |
| # classes and one with the PHP implementations, in separate processes; | |
| # the traces must be identical. It compares the engine ports as the | |
| # engine runs them (native calling native under the real class names), | |
| # which the prefixed single-process harnesses in smoke.php cannot. One | |
| # leg is enough: it checks engine behaviour, not the platform. | |
| - name: "Walk trace (differential: whole NodeScopeResolver walks, native vs PHP)" | |
| if: matrix.target == 'linux-gnu-x86_64' && matrix.php-version == '8.5' | |
| run: php -d extension="$TURBO_DLL" turbo-ext/tests/walk-trace.php --shards="$(nproc)" | |
| - name: "Arena smoke test (cross-process shared-memory records)" | |
| run: php -d extension="$TURBO_DLL" turbo-ext/tests/arena-smoke.php | |
| - name: "Forked-worker exit (exitImmediately() ends a child whose teardown would wedge)" | |
| run: php -d extension="$TURBO_DLL" turbo-ext/tests/exit-immediately.php | |
| - name: "Trusted types (the optimizer pass drops type checks under the prefix only)" | |
| run: php -d extension="$TURBO_DLL" -d opcache.enable_cli=1 turbo-ext/tests/trusted-types.php | |
| - name: "Signature parity (reflect native classes against the PHP twins)" | |
| run: php -d extension="$TURBO_DLL" turbo-ext/tests/signature-parity.php | |
| - name: "Parser corpus (differential: native vs PHP ASTs must be byte-identical)" | |
| run: php -d extension="$TURBO_DLL" -d memory_limit=4G turbo-ext/tests/parser-corpus.php | |
| - name: "Parser upstream corpus (the same differential over php-parser's own test cases)" | |
| if: matrix.target != 'windows-x86_64' | |
| run: php -d extension="$TURBO_DLL" turbo-ext/tests/parser-upstream-corpus.php | |
| turbo-differential-musl: | |
| name: "Run with Turbo Extension (differential tests, musl)" | |
| # turbo-differential for the musl binaries, in the Alpine environment | |
| # they are built in. The commands run through docker exec for the reason | |
| # turbo-compile-musl-arm64 gives, on both architectures alike. | |
| needs: | |
| - turbo-compile | |
| - turbo-compile-musl-arm64 | |
| runs-on: ${{ matrix.arch == 'arm64' && 'ubuntu-24.04-arm' || 'ubuntu-latest' }} | |
| timeout-minutes: 30 | |
| env: | |
| PHP_MINOR: ${{ matrix.php-version }} | |
| PHP_ZTS: ${{ matrix.ts == 'zts' && '1' || '0' }} | |
| TURBO_ARTIFACT: "phpstan_turbo-linux-musl-${{ matrix.arch }}-php${{ matrix.php-version }}${{ matrix.ts == 'zts' && '-zts' || '' }}" | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| arch: ["x86_64", "arm64"] | |
| php-version: ["8.3", "8.4", "8.5", "8.6"] | |
| ts: ["nts"] | |
| # thread-safe from 8.6 on, see turbo-compile | |
| include: | |
| - arch: "x86_64" | |
| php-version: "8.6" | |
| ts: "zts" | |
| - arch: "arm64" | |
| php-version: "8.6" | |
| ts: "zts" | |
| steps: | |
| - name: Harden the runner (Audit all outbound calls) | |
| uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 | |
| with: | |
| egress-policy: audit | |
| - name: "Checkout" | |
| uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 | |
| with: | |
| persist-credentials: false | |
| # The same container, packages and composer as turbo-compile-musl-arm64. | |
| - name: "Start the Alpine container" | |
| env: | |
| ALPINE_IMAGE: ${{ matrix.php-version == '8.6' && matrix.ts == 'nts' && 'alpine:edge' || 'alpine:3.24' }} | |
| run: | | |
| docker run -d --name alpine-test \ | |
| -v "$PWD:/work" -w /work \ | |
| -e PHP_MINOR -e PHP_ZTS -e COMPOSER_VERSION -e COMPOSER_PHAR_SHA256 \ | |
| "$ALPINE_IMAGE" sleep 7200 | |
| - name: "Install PHP and tools" | |
| run: docker exec alpine-test sh .github/scripts/install-alpine-php.sh | |
| - name: "Trust the checkout despite the container/host uid mismatch" | |
| run: docker exec alpine-test git config --global --add safe.directory /work | |
| - name: "Install Composer dependencies (pinned composer)" | |
| run: | | |
| docker exec -i alpine-test sh -e <<'EOF' | |
| curl -fsSLo composer.phar "https://getcomposer.org/download/$COMPOSER_VERSION/composer.phar" | |
| echo "$COMPOSER_PHAR_SHA256 composer.phar" | sha256sum -c - | |
| COMPOSER_HOME="$(mktemp -d)" php composer.phar install --no-interaction --no-progress | |
| rm composer.phar | |
| EOF | |
| - name: "Download extension artifact" | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: ${{ env.TURBO_ARTIFACT }} | |
| path: "turbo-ext" | |
| - name: "Verify the extension reports the expected version" | |
| run: | | |
| docker exec -i alpine-test sh -e <<'EOF' | |
| REPORTED="$(php -d extension=/work/turbo-ext/phpstan_turbo.so -r 'echo phpversion("phpstan_turbo");')" | |
| EXPECTED="$(sed -n "s/.*EXPECTED_EXTENSION_VERSION = '\([^']*\)'.*/\1/p" src/Turbo/TurboExtensionEnabler.php)" | |
| echo "extension reports: $REPORTED, enabler expects: $EXPECTED" | |
| [ "$REPORTED" = "$EXPECTED" ] | |
| [ "$(php -r 'echo (int) ((bool) PHP_ZTS);')" = "$PHP_ZTS" ] | |
| EOF | |
| - name: "Smoke test (differential: native vs PHP implementations)" | |
| run: docker exec alpine-test php -d extension=/work/turbo-ext/phpstan_turbo.so turbo-ext/tests/smoke.php | |
| - name: "Arena smoke test (cross-process shared-memory records)" | |
| run: docker exec alpine-test php -d extension=/work/turbo-ext/phpstan_turbo.so turbo-ext/tests/arena-smoke.php | |
| - name: "Forked-worker exit (exitImmediately() ends a child whose teardown would wedge)" | |
| run: docker exec alpine-test php -d extension=/work/turbo-ext/phpstan_turbo.so turbo-ext/tests/exit-immediately.php | |
| - name: "Trusted types (the optimizer pass drops type checks under the prefix only)" | |
| run: docker exec alpine-test php -d extension=/work/turbo-ext/phpstan_turbo.so -d opcache.enable_cli=1 turbo-ext/tests/trusted-types.php | |
| - name: "Signature parity (reflect native classes against the PHP twins)" | |
| run: docker exec alpine-test php -d extension=/work/turbo-ext/phpstan_turbo.so turbo-ext/tests/signature-parity.php | |
| - name: "Parser corpus (differential: native vs PHP ASTs must be byte-identical)" | |
| run: docker exec alpine-test php -d extension=/work/turbo-ext/phpstan_turbo.so -d memory_limit=4G turbo-ext/tests/parser-corpus.php | |
| - name: "Parser upstream corpus (the same differential over php-parser's own test cases)" | |
| if: matrix.arch == 'x86_64' | |
| run: docker exec alpine-test php -d extension=/work/turbo-ext/phpstan_turbo.so turbo-ext/tests/parser-upstream-corpus.php | |
| turbo-run: | |
| name: "Run with Turbo Extension" | |
| needs: | |
| - turbo-compile | |
| - turbo-compile-windows | |
| runs-on: ${{ matrix.operating-system }} | |
| timeout-minutes: 60 | |
| defaults: | |
| run: | |
| shell: bash | |
| strategy: | |
| fail-fast: false | |
| # windows-latest only runs the DLL, it cannot build it: the DLL is | |
| # linked with the 14.4x toolset on windows-2022, and the loader's | |
| # linker-generation gate is one-directional (module <= core), so the | |
| # vs17-built official PHP loads it fine. | |
| matrix: | |
| operating-system: ["ubuntu-latest", "windows-latest"] | |
| php-version: ["8.3", "8.4", "8.5", "8.6"] | |
| ts: ["nts", "zts"] | |
| script: ["make tests", "make phpstan"] | |
| # Linux ships ZTS binaries only from 8.6 on (see turbo-compile), and | |
| # the ubuntu 8.6 legs are excluded below — turbo-docker-run runs the | |
| # 8.6 ZTS binaries in the official, thread-safe 8.6 images instead. | |
| exclude: | |
| - operating-system: "ubuntu-latest" | |
| ts: "zts" | |
| # setup-php's Linux 8.6 is a nightly snapshot that can predate the | |
| # release the binary is compiled against (on 2026-09-25 it still | |
| # had the module API 8.6.0RC2 replaced), and a module API mismatch | |
| # refuses to load the extension at all. turbo-docker-run runs the | |
| # same checks with this binary in the official 8.6 prerelease image. | |
| - operating-system: "ubuntu-latest" | |
| php-version: "8.6" | |
| # macOS runs the shipped ad-hoc-signed arm64 binary; there is no | |
| # macOS zts build, and 8.3/8.4 coverage comes from the other legs. | |
| include: | |
| - operating-system: "macos-latest" | |
| php-version: "8.5" | |
| ts: "nts" | |
| script: "make tests" | |
| - operating-system: "macos-latest" | |
| php-version: "8.5" | |
| ts: "nts" | |
| script: "make phpstan" | |
| - operating-system: "macos-latest" | |
| php-version: "8.6" | |
| ts: "nts" | |
| script: "make tests" | |
| - operating-system: "macos-latest" | |
| php-version: "8.6" | |
| ts: "nts" | |
| script: "make phpstan" | |
| steps: | |
| - name: Harden the runner (Audit all outbound calls) | |
| uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 | |
| with: | |
| egress-policy: audit | |
| - name: "Checkout" | |
| uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 | |
| with: | |
| persist-credentials: false | |
| - name: "Install PHP" | |
| uses: "shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240" # v2.37.2 | |
| env: | |
| phpts: "${{ matrix.ts == 'zts' && 'ts' || 'nts' }}" | |
| with: | |
| coverage: "none" | |
| php-version: "${{ matrix.php-version }}" | |
| - name: "Install the matching PHP 8.6 prerelease runtime" | |
| if: matrix.operating-system == 'windows-latest' && matrix.php-version == '8.6' | |
| env: | |
| MATRIX_TS: ${{ matrix.ts }} | |
| run: bash .github/scripts/install-php86-windows.sh | |
| - name: "Download extension artifact" | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: "phpstan_turbo-${{ matrix.operating-system == 'windows-latest' && 'windows-x86_64' || matrix.operating-system == 'macos-latest' && 'macos-arm64' || 'linux-gnu-x86_64' }}-php${{ matrix.php-version }}${{ matrix.ts == 'zts' && '-zts' || '' }}" | |
| path: "turbo-ext" | |
| - name: "Install extension" | |
| # The ini line needs a native path and the redirection a POSIX one, | |
| # hence the cygpath round-trip on Windows; setup-php's Linux ini is | |
| # root-owned, hence the sudo fallback. | |
| run: | | |
| FILE="$(find turbo-ext -maxdepth 1 \( -name phpstan_turbo.so -o -name php_phpstan_turbo.dll \) | head -1)" | |
| [ -n "$FILE" ] | |
| EXT="$PWD/$FILE" | |
| INI="$(php -r 'echo php_ini_loaded_file();')" | |
| if command -v cygpath > /dev/null; then | |
| EXT="$(cygpath -w "$EXT")" | |
| INI="$(cygpath -u "$INI")" | |
| fi | |
| if [ -w "$INI" ]; then | |
| echo "extension=$EXT" >> "$INI" | |
| else | |
| sudo bash -c "echo 'extension=$EXT' >> '$INI'" | |
| fi | |
| php -m | grep phpstan_turbo | |
| - uses: "ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda" # v4.0.0 | |
| with: | |
| ignore-cache: true | |
| - name: "Verify the extension is active" | |
| run: | | |
| php -r ' | |
| require "vendor/autoload.php"; | |
| PHPStan\Turbo\TurboExtensionEnabler::activateIfCompatible(); | |
| if (!PHPStan\Turbo\TurboExtensionEnabler::isActive() || !(new ReflectionMethod("PHPStan\\Analyser\\ScopeOps", "nodeKey"))->isInternal()) { | |
| fwrite(STDERR, "turbo extension is not shadowing ScopeOps" . PHP_EOL); | |
| exit(1); | |
| } | |
| ' | |
| - name: "Run" | |
| # The Windows runner image ships GNU make only inside its MSYS2 | |
| # install, which is not on PATH. | |
| env: | |
| SCRIPT: ${{ matrix.script }} | |
| run: | | |
| if ! command -v make > /dev/null && [ -e /c/msys64/usr/bin/make.exe ]; then | |
| export PATH="$PATH:/c/msys64/usr/bin" | |
| fi | |
| eval "$SCRIPT" | |
| phar-run: | |
| name: "Run PHAR on PHP ${{ matrix.php-version }}" | |
| needs: compiler-tests | |
| runs-on: "ubuntu-latest" | |
| timeout-minutes: 20 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| php-version: ["7.4", "8.0", "8.1", "8.2", "8.3", "8.4", "8.5", "8.6"] | |
| steps: | |
| - name: Harden the runner (Audit all outbound calls) | |
| uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 | |
| with: | |
| egress-policy: audit | |
| - name: "Checkout" | |
| uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 | |
| with: | |
| persist-credentials: false | |
| - name: "Install PHP" | |
| # Deliberately without `ini-file: development`: that ini lists the | |
| # pcntl functions in disable_functions, and TurboProcessRestarter then | |
| # returns before TurboExtensionSelector ever runs. With pcntl available | |
| # the phar boots through the turbo restart code, which bin/phpstan | |
| # loads before the Composer autoloader - on PHP < 8.0 without the | |
| # symfony polyfills (https://github.com/phpstan/phpstan/issues/15137). | |
| uses: "shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240" # v2.37.2 | |
| with: | |
| coverage: "none" | |
| php-version: "${{ matrix.php-version }}" | |
| - name: "Download PHAR" | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: phar-file | |
| path: tmp | |
| - name: "Verify the turbo restart path is reachable" | |
| run: | | |
| php -r 'exit(function_exists("pcntl_exec") && function_exists("pcntl_fork") ? 0 : 1);' | |
| - name: "Run PHAR" | |
| # No turbo binary sits next to this phar (the turbo-* jobs cover | |
| # those), so ForkParallelChecker spawns the two workers phpstan.neon | |
| # asks for. At -vvv each reports what it runs with: the OPcache | |
| # setup its command line carries (see ProcessHelper) took effect. | |
| run: | | |
| php tmp/phpstan.phar --version | |
| cd e2e/phar-run | |
| php ../../tmp/phpstan.phar analyse -vvv 2>&1 | tee output.txt | |
| OPCACHE="$(php -r 'echo extension_loaded("Zend OPcache") ? "on" : "off";')" | |
| grep -F "Spawned worker 1/2 checked in: turbo off, OPcache $OPCACHE, trusted types off" output.txt | |
| grep -F "Spawned worker 2/2 checked in: turbo off, OPcache $OPCACHE, trusted types off" output.txt | |
| - name: "Reuse the result cache" | |
| # Control for the same step of phar-run-windows, see | |
| # https://github.com/phpstan/phpstan/issues/15152: a second run of the unchanged | |
| # project must restore the cache the first run wrote. | |
| run: | | |
| cd e2e/phar-run | |
| php ../../tmp/phpstan.phar analyse -vv 2>&1 | tee output-warm.txt | |
| grep -F "Result cache restored. 0 files will be reanalysed." output-warm.txt | |
| phar-run-windows: | |
| name: "Run PHAR on Windows, PHP ${{ matrix.php-version }}" | |
| needs: | |
| - compiler-tests | |
| - turbo-compile-windows | |
| runs-on: "windows-latest" | |
| timeout-minutes: 20 | |
| defaults: | |
| run: | |
| shell: bash | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| # below TurboExtensionSelector::MINIMUM_PHP_VERSION_ID there is no | |
| # binary to find: the workers get the OPcache entries alone | |
| - php-version: "8.2" | |
| turbo: "off" | |
| - php-version: "8.5" | |
| turbo: "on" | |
| steps: | |
| - name: "Checkout" | |
| uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 | |
| with: | |
| persist-credentials: false | |
| - name: "Install PHP" | |
| uses: "shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240" # v2.37.2 | |
| with: | |
| coverage: "none" | |
| php-version: "${{ matrix.php-version }}" | |
| extensions: "opcache" | |
| - name: "Download PHAR" | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: phar-file | |
| path: tmp | |
| - name: "Download extension artifact" | |
| if: matrix.turbo == 'on' | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: "phpstan_turbo-windows-x86_64-php${{ matrix.php-version }}" | |
| path: "tmp/turbo-artifact" | |
| - name: "Place the extension next to the PHAR" | |
| # the distribution layout TurboExtensionSelector looks for, see the | |
| # turbo-artifact job | |
| if: matrix.turbo == 'on' | |
| env: | |
| PHP_VERSION: ${{ matrix.php-version }} | |
| run: | | |
| mkdir -p tmp/turbo-ext/windows-x86_64 | |
| cp tmp/turbo-artifact/php_phpstan_turbo.dll "tmp/turbo-ext/windows-x86_64/phpstan_turbo-$PHP_VERSION.dll" | |
| - name: "Diagnose" | |
| # Without pcntl the main process cannot restart itself, so the | |
| # workers are spawned and their command line is where the extension | |
| # and OPcache come in - each worker with its own OPcache instance | |
| # (see ProcessHelper). | |
| env: | |
| TURBO: ${{ matrix.turbo }} | |
| run: | | |
| cd e2e/phar-run | |
| php ../../tmp/phpstan.phar diagnose --no-ansi 2>&1 | tee diagnose.txt | |
| grep -F "Mechanism: spawn (react/child-process)" diagnose.txt | |
| grep -F "opcache.enable_cli=1" diagnose.txt | |
| grep -E "opcache\.cache_id=phpstan-[0-9]+-1" diagnose.txt | |
| grep -F "phpstan.restarted=1" diagnose.txt | |
| if [ "$TURBO" = on ]; then | |
| grep -F "Turbo extension: enabled in worker processes" diagnose.txt | |
| else | |
| grep -F "Turbo extension: not loaded" diagnose.txt | |
| fi | |
| - name: "Run PHAR" | |
| env: | |
| TURBO: ${{ matrix.turbo }} | |
| run: | | |
| php tmp/phpstan.phar --version | |
| cd e2e/phar-run | |
| php ../../tmp/phpstan.phar analyse -vvv --no-ansi 2>&1 | tee output.txt | |
| grep -F "Spawned worker 1/2 checked in: turbo $TURBO, OPcache on, trusted types $TURBO" output.txt | |
| grep -F "Spawned worker 2/2 checked in: turbo $TURBO, OPcache on, trusted types $TURBO" output.txt | |
| - name: "Reuse the result cache" | |
| # https://github.com/phpstan/phpstan/issues/15152: since 2.2.9 a second run of an | |
| # unchanged project on Windows is reported to discard the cache with "metadata do not | |
| # match: ... executedFilesHashes" - the phar:// URLs of the runtime stubs registered as | |
| # bootstrapFiles. Every invocation form is tried and all of them are reported before | |
| # the step fails. | |
| run: | | |
| cd e2e/phar-run | |
| FAILED=0 | |
| check() { | |
| if grep -qF "Result cache restored. 0 files will be reanalysed." "$1"; then | |
| echo "OK: $2" | |
| else | |
| echo "::error::$2: the result cache was not reused" | |
| grep -F "Result cache" "$1" || true | |
| FAILED=1 | |
| fi | |
| } | |
| echo "::group::Second run through the relative path" | |
| php ../../tmp/phpstan.phar analyse -vv --no-ansi 2>&1 | tee output-relative.txt | |
| echo "::endgroup::" | |
| check output-relative.txt "relative invocation after the first run" | |
| # the invocation form the issue describes | |
| PHAR="$(cygpath -w "$GITHUB_WORKSPACE/tmp/phpstan.phar")" | |
| echo "::group::Runs through the absolute path $PHAR" | |
| php "$PHAR" analyse -vv --no-ansi 2>&1 | tee output-absolute-1.txt | |
| php "$PHAR" analyse -vv --no-ansi 2>&1 | tee output-absolute-2.txt | |
| echo "::endgroup::" | |
| check output-absolute-1.txt "absolute invocation after the relative ones" | |
| check output-absolute-2.txt "absolute invocation after an absolute one" | |
| # vendor/phpstan/phpstan/phpstan, the entry point of a Composer install, maps the | |
| # archive under its alias and requires bin/phpstan through it | |
| printf '%s\n' '<?php' "Phar::loadPhar(__DIR__ . '/phpstan.phar', 'phpstan.phar');" "require 'phar://phpstan.phar/bin/phpstan';" > ../../tmp/phpstan | |
| echo "::group::Runs through a Composer-style wrapper" | |
| php ../../tmp/phpstan analyse -vv --no-ansi 2>&1 | tee output-wrapper-1.txt | |
| php ../../tmp/phpstan analyse -vv --no-ansi 2>&1 | tee output-wrapper-2.txt | |
| echo "::endgroup::" | |
| check output-wrapper-1.txt "wrapper invocation after the direct ones" | |
| check output-wrapper-2.txt "wrapper invocation after a wrapper one" | |
| exit "$FAILED" | |
| integration-tests: | |
| if: github.event_name == 'pull_request' | |
| needs: | |
| - compiler-tests | |
| - turbo-artifact | |
| uses: phpstan/phpstan/.github/workflows/integration-tests.yml@2.3.x | |
| with: | |
| ref: 2.3.x | |
| phar-checksum: ${{needs.compiler-tests.outputs.checksum}} | |
| extension-tests: | |
| if: github.event_name == 'pull_request' | |
| needs: | |
| - compiler-tests | |
| - turbo-artifact | |
| uses: phpstan/phpstan/.github/workflows/extension-tests.yml@2.3.x | |
| with: | |
| ref: 2.3.x | |
| phar-checksum: ${{needs.compiler-tests.outputs.checksum}} | |
| other-tests: | |
| if: github.event_name == 'pull_request' | |
| needs: | |
| - compiler-tests | |
| - turbo-artifact | |
| uses: phpstan/phpstan/.github/workflows/other-tests.yml@2.3.x | |
| with: | |
| ref: 2.3.x | |
| phar-checksum: ${{needs.compiler-tests.outputs.checksum}} | |
| download-base-sha-phar: | |
| name: "Download base SHA PHAR" | |
| needs: compiler-tests | |
| if: github.event_name == 'pull_request' && needs.compiler-tests.outputs.compiler_changed == 'true' | |
| runs-on: "ubuntu-latest" | |
| permissions: | |
| contents: read # actions/checkout of this repository | |
| actions: read # find-artifact.js lists runs/artifacts; the by-ID cross-run download authenticates with the workflow token | |
| steps: | |
| - name: Harden the runner (Audit all outbound calls) | |
| uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 | |
| with: | |
| egress-policy: audit | |
| - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 | |
| with: | |
| persist-credentials: false | |
| - name: Get base commit SHA | |
| id: base | |
| env: | |
| BASE_SHA: ${{ github.event.pull_request.base.sha }} | |
| run: echo "base_sha=$BASE_SHA" >> "$GITHUB_OUTPUT" | |
| - name: Set up Node.js | |
| uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0 | |
| with: | |
| node-version: 20 | |
| package-manager-cache: false | |
| - name: Install dependencies | |
| working-directory: .github/scripts | |
| run: npm ci | |
| - name: "Compile TS scripts" | |
| working-directory: .github/scripts | |
| run: npx tsc | |
| - name: Find phar-file-checksum from base commit | |
| id: find-artifact | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| env: | |
| BASE_SHA: ${{ steps.base.outputs.base_sha }} | |
| ARTIFACT_NAME: phar-file-checksum | |
| WORKFLOW_NAME: Compile PHAR | |
| with: | |
| script: | | |
| const script = require('./.github/scripts/dist/find-artifact.js'); | |
| await script({github, context, core}) | |
| # saved to phar-file-checksum/phpstan.phar | |
| - name: Download old artifact by ID | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| artifact-ids: ${{ steps.find-artifact.outputs.artifact_id }} | |
| run-id: ${{ steps.find-artifact.outputs.run_id }} | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| # download-artifact v5+ extracts single by-ID downloads directly into | |
| # `path`, no longer nested under the artifact name; keep the old layout | |
| path: phar-file-checksum | |
| - name: "Upload old artifact" | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: phar-file-checksum-base | |
| path: phar-file-checksum/phpstan.phar | |
| checksum-phar: | |
| name: "Checksum PHAR" | |
| needs: | |
| - compiler-tests | |
| - download-base-sha-phar | |
| runs-on: "ubuntu-latest" | |
| steps: | |
| # saved to phpstan.phar | |
| - name: Harden the runner (Audit all outbound calls) | |
| uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 | |
| with: | |
| egress-policy: audit | |
| - name: "Download base phpstan.phar" | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: phar-file-checksum-base | |
| - name: "Save old checksum" | |
| id: "old_checksum" | |
| run: echo "md5=$(md5sum phpstan.phar | cut -d' ' -f1)" >> "$GITHUB_OUTPUT" | |
| - name: "Assert checksum" | |
| env: | |
| OLD_CHECKSUM: ${{ steps.old_checksum.outputs.md5 }} | |
| NEW_CHECKSUM: ${{ needs.compiler-tests.outputs.checksum }} | |
| run: | | |
| [[ "$OLD_CHECKSUM" == "$NEW_CHECKSUM" ]]; | |
| phar-prefix-diff: | |
| name: "PHAR Prefix Diff" | |
| needs: download-base-sha-phar | |
| runs-on: "ubuntu-latest" | |
| steps: | |
| - name: Harden the runner (Audit all outbound calls) | |
| uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 | |
| with: | |
| egress-policy: audit | |
| - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 | |
| with: | |
| persist-credentials: false | |
| # saved to phar-file-checksum/phpstan.phar | |
| - name: "Download phpstan.phar" | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: phar-file-checksum | |
| path: phar-file-checksum | |
| # saved to phar-file-checksum-base/phpstan.phar | |
| - name: "Download base phpstan.phar" | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: phar-file-checksum-base | |
| path: phar-file-checksum-base | |
| - name: "Install PHP" | |
| uses: "shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240" # 2.37.2 | |
| with: | |
| coverage: "none" | |
| php-version: "8.2" | |
| - uses: "ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda" # v4.0.0 | |
| with: | |
| ignore-cache: true | |
| - name: "Install Box dependencies" | |
| uses: "ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda" # v4.0.0 | |
| with: | |
| working-directory: "compiler/box" | |
| ignore-cache: true | |
| - name: "Extract old phpstan.phar" | |
| run: "php compiler/box/vendor/bin/box extract phar-file-checksum-base/phpstan.phar phar-old" | |
| - name: "Extract new phpstan.phar" | |
| run: "php compiler/box/vendor/bin/box extract phar-file-checksum/phpstan.phar phar-new" | |
| - name: "List prefix locations in old PHAR" | |
| run: 'php .github/scripts/listPrefix.php "$GITHUB_WORKSPACE/phar-old" > phar-old.txt' | |
| - name: "List prefix locations in new PHAR" | |
| run: 'php .github/scripts/listPrefix.php "$GITHUB_WORKSPACE/phar-new" > phar-new.txt' | |
| - name: "Diff locations" | |
| run: "diff -u phar-old.txt phar-new.txt > diff.txt || true" | |
| - name: "Diff files where prefix changed" | |
| run: 'php .github/scripts/diffPrefixes.php "$GITHUB_WORKSPACE/diff.txt" "$GITHUB_WORKSPACE/phar-old" "$GITHUB_WORKSPACE/phar-new"' | |
| commit: | |
| name: "Commit PHAR" | |
| if: "github.repository_owner == 'phpstan' && (github.ref == 'refs/heads/2.3.x' || startsWith(github.ref, 'refs/tags/'))" | |
| # Gated on every job that produces the committed bits or the version | |
| # constant they are staged by: the compile jobs build the staged | |
| # binaries (or carry over ones compiled from identical build inputs, | |
| # see turbo-origins) and check the version they report; turbo-version | |
| # guards the version constant the staging logic compares. The jobs | |
| # that only test those bits are deliberately not gates: the | |
| # "Run with Turbo Extension" jobs (turbo-differential, | |
| # turbo-differential-musl, turbo-run, turbo-docker-run) and | |
| # turbo-phpize, whose build nothing ships (PIE | |
| # builds from the phpstan/turbo-ext subsplit). They produce nothing the | |
| # commit consumes, waiting for them would delay every dev phar by their | |
| # duration, and a red leg caused by environment drift (the floating | |
| # docker-library tags, the PHP 8.6 prerelease) would skip the commit and | |
| # halt phar publication while the binaries already in dist are equally | |
| # affected — nothing protective gained. They still fail the workflow | |
| # loudly for follow-up. | |
| needs: | |
| - compiler-tests | |
| - turbo-version | |
| - turbo-compile | |
| - turbo-compile-musl-arm64 | |
| - turbo-compile-windows | |
| runs-on: "ubuntu-latest" | |
| timeout-minutes: 60 | |
| steps: | |
| - name: Harden the runner (Audit all outbound calls) | |
| uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 | |
| with: | |
| egress-policy: audit | |
| - | |
| name: Import GPG key | |
| id: import-gpg | |
| uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7.0.0 | |
| with: | |
| gpg_private_key: ${{ secrets.GPG_PHPSTANBOT_PRIVATE_KEY }} | |
| passphrase: ${{ secrets.GPG_PHPSTANBOT_KEY_PASSPHRASE }} | |
| git_config_global: true | |
| git_user_signingkey: true | |
| git_commit_gpgsign: true | |
| - name: "Checkout phpstan-dist" | |
| uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 | |
| with: | |
| repository: phpstan/phpstan | |
| path: phpstan-dist | |
| token: ${{ secrets.PHPSTAN_BOT_TOKEN }} | |
| ref: 2.3.x | |
| # deliberately persisted: the push steps below authenticate with | |
| # this bot token | |
| persist-credentials: true | |
| - name: "Get previous pushed dist commit" | |
| id: previous-commit | |
| working-directory: phpstan-dist | |
| run: echo "sha=$(sed -n '2p' .phar-checksum)" >> "$GITHUB_OUTPUT" | |
| - name: "Checkout phpstan-src" | |
| uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 | |
| with: | |
| persist-credentials: false | |
| fetch-depth: 0 | |
| path: phpstan-src | |
| - name: "Get Git log" | |
| id: git-log | |
| working-directory: phpstan-src | |
| env: | |
| PREVIOUS_SHA: ${{ steps.previous-commit.outputs.sha }} | |
| AFTER_SHA: ${{ github.event.after }} | |
| run: | | |
| echo "log<<MESSAGE" >> "$GITHUB_OUTPUT" | |
| git log "$PREVIOUS_SHA".."$AFTER_SHA" --reverse --pretty='https://github.com/phpstan/phpstan-src/commit/%H %s' >> "$GITHUB_OUTPUT" | |
| echo 'MESSAGE' >> "$GITHUB_OUTPUT" | |
| - name: "Get short phpstan-src SHA" | |
| id: short-src-sha | |
| working-directory: phpstan-src | |
| run: echo "sha=$(git rev-parse --short=7 HEAD)" >> "$GITHUB_OUTPUT" | |
| - name: "Check PHAR checksum" | |
| id: checksum-difference | |
| working-directory: phpstan-dist | |
| env: | |
| CHECKSUM: ${{ needs.compiler-tests.outputs.checksum }} | |
| run: | | |
| checksum="$CHECKSUM" | |
| if [[ $(head -n 1 .phar-checksum) != "$checksum" ]]; then | |
| echo "result=different" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "result=same" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: "Download turbo extension artifacts" | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| pattern: "phpstan_turbo-*" | |
| path: "turbo-artifacts" | |
| - name: "Check turbo extension version and binary set" | |
| id: turbo-difference | |
| run: | | |
| EXPECTED="$(sed -n "s/.*EXPECTED_EXTENSION_VERSION = '\([^']*\)'.*/\1/p" phpstan-src/src/Turbo/TurboExtensionEnabler.php)" | |
| CURRENT="$(cat phpstan-dist/turbo-ext/.version 2>/dev/null || echo none)" | |
| echo "enabler expects: $EXPECTED, dist carries: $CURRENT" | |
| RESULT=same | |
| VERSION_CHANGED=no | |
| if [[ "$EXPECTED" != "$CURRENT" ]]; then | |
| RESULT=different | |
| VERSION_CHANGED=yes | |
| fi | |
| # A new compile-matrix leg (new PHP minor or platform) produces | |
| # binaries the dist does not carry even though the extension | |
| # version is unchanged. | |
| for dir in turbo-artifacts/phpstan_turbo-*; do | |
| [ -d "$dir" ] || continue | |
| name="${dir#turbo-artifacts/phpstan_turbo-}" | |
| target="${name%-php*}" | |
| minor="${name##*-php}" | |
| for file in "$dir"/*; do | |
| dist_file="phpstan-dist/turbo-ext/$target/phpstan_turbo-$minor.${file##*.}" | |
| if [[ ! -f "$dist_file" ]]; then | |
| echo "missing from dist: $dist_file" | |
| RESULT=different | |
| fi | |
| done | |
| done | |
| # A retired binary still in the dist needs a commit deleting it, | |
| # even at an unchanged extension version. | |
| for pattern in $TURBO_RETIRED_BINARIES; do | |
| for retired in phpstan-dist/turbo-ext/$pattern; do | |
| if [[ -f "$retired" ]]; then | |
| echo "retired, to be deleted from dist: $retired" | |
| RESULT=different | |
| fi | |
| done | |
| done | |
| echo "result=$RESULT" >> "$GITHUB_OUTPUT" | |
| echo "version-changed=$VERSION_CHANGED" >> "$GITHUB_OUTPUT" | |
| echo "version=$EXPECTED" >> "$GITHUB_OUTPUT" | |
| # The phar is staged only when its checksum changed (or on tags, which | |
| # always produce a commit) — a turbo-only update must not churn the | |
| # byte-unstable phar; existing turbo binaries are overwritten only when | |
| # the extension version changed, since native builds are never | |
| # byte-reproducible (signing timestamps, Mach-O UUIDs). Binaries the | |
| # dist does not carry at all are staged even at an unchanged version. | |
| - name: "Download phpstan.phar" | |
| if: startsWith(github.ref, 'refs/tags/') || steps.checksum-difference.outputs.result == 'different' | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: phar-file | |
| - name: "mv PHAR" | |
| if: startsWith(github.ref, 'refs/tags/') || steps.checksum-difference.outputs.result == 'different' | |
| run: mv phpstan.phar phpstan-dist/phpstan.phar | |
| - name: "chmod PHAR" | |
| if: startsWith(github.ref, 'refs/tags/') || steps.checksum-difference.outputs.result == 'different' | |
| run: chmod 755 phpstan-dist/phpstan.phar | |
| - name: "Update checksum" | |
| if: startsWith(github.ref, 'refs/tags/') || steps.checksum-difference.outputs.result == 'different' | |
| env: | |
| CHECKSUM: ${{ needs.compiler-tests.outputs.checksum }} | |
| HEAD_COMMIT_ID: ${{ github.event.head_commit.id }} | |
| run: | | |
| echo "$CHECKSUM" > phpstan-dist/.phar-checksum | |
| echo "$HEAD_COMMIT_ID" >> phpstan-dist/.phar-checksum | |
| - name: "Sign PHAR" | |
| if: startsWith(github.ref, 'refs/tags/') || steps.checksum-difference.outputs.result == 'different' | |
| working-directory: phpstan-dist | |
| run: rm phpstan.phar.asc && gpg --command-fd 0 --pinentry-mode loopback -u "$GPG_ID" --batch --detach-sign --armor --output phpstan.phar.asc phpstan.phar | |
| env: | |
| GPG_ID: ${{ steps.import-gpg.outputs.fingerprint }} | |
| - name: "Verify PHAR" | |
| if: startsWith(github.ref, 'refs/tags/') || steps.checksum-difference.outputs.result == 'different' | |
| working-directory: phpstan-dist | |
| run: "gpg --verify phpstan.phar.asc" | |
| - name: "Stage turbo extension binaries" | |
| if: steps.turbo-difference.outputs.result == 'different' | |
| env: | |
| TURBO_VERSION: ${{ steps.turbo-difference.outputs.version }} | |
| VERSION_CHANGED: ${{ steps.turbo-difference.outputs.version-changed }} | |
| run: | | |
| # Retired targets go first: they have no replacement by design, and | |
| # a stale one must not keep shipping (`git add .` stages the deletion). | |
| for pattern in $TURBO_RETIRED_BINARIES; do | |
| for retired in phpstan-dist/turbo-ext/$pattern; do | |
| if [[ -f "$retired" ]]; then | |
| echo "deleting retired binary: $retired" | |
| rm -f "$retired" | |
| fi | |
| done | |
| done | |
| # A version bump must refresh EVERY binary the dist carries: staging a | |
| # partial artifact set and bumping .version anyway would leave the | |
| # missing platforms on a stale extension the new phar refuses to | |
| # activate - permanently, because later runs compare .version and see | |
| # "same". Fail loudly instead of committing a torn set. | |
| if [[ "$VERSION_CHANGED" == "yes" ]]; then | |
| MISSING="" | |
| if ! ls turbo-artifacts/phpstan_turbo-* >/dev/null 2>&1; then | |
| echo "::error::no turbo extension artifacts were downloaded" | |
| MISSING=yes | |
| fi | |
| for dist_file in phpstan-dist/turbo-ext/*/phpstan_turbo-*; do | |
| [ -f "$dist_file" ] || continue | |
| rel="${dist_file#phpstan-dist/turbo-ext/}" | |
| target="${rel%%/*}" | |
| base="${rel##*/}" | |
| minor="${base#phpstan_turbo-}" | |
| minor="${minor%.*}" | |
| if ! ls "turbo-artifacts/phpstan_turbo-$target-php$minor"/* >/dev/null 2>&1; then | |
| echo "::error::$dist_file has no freshly built replacement (artifact phpstan_turbo-$target-php$minor missing) - refusing to bump turbo-ext/.version over a stale binary." | |
| MISSING=yes | |
| fi | |
| done | |
| if [[ -n "$MISSING" ]]; then | |
| exit 1 | |
| fi | |
| fi | |
| for dir in turbo-artifacts/phpstan_turbo-*; do | |
| [ -d "$dir" ] || continue | |
| name="${dir#turbo-artifacts/phpstan_turbo-}" | |
| target="${name%-php*}" | |
| minor="${name##*-php}" | |
| for file in "$dir"/*; do | |
| dest="phpstan-dist/turbo-ext/$target/phpstan_turbo-$minor.${file##*.}" | |
| if [[ "$VERSION_CHANGED" == "yes" || ! -f "$dest" ]]; then | |
| install -D -m 644 "$file" "$dest" | |
| fi | |
| done | |
| done | |
| echo "$TURBO_VERSION" > phpstan-dist/turbo-ext/.version | |
| - name: "Install lucky_commit" | |
| uses: baptiste0928/cargo-install@f204293d9709061b7bc1756fec3ec4e2cd57dec0 # v3.4.0 | |
| with: | |
| crate: lucky_commit | |
| args: --no-default-features | |
| - name: "Commit PHAR - development" | |
| if: "!startsWith(github.ref, 'refs/tags/') && (steps.checksum-difference.outputs.result == 'different' || steps.turbo-difference.outputs.result == 'different')" | |
| working-directory: phpstan-dist | |
| env: | |
| INPUT_LOG: ${{ steps.git-log.outputs.log }} | |
| AFTER_SHA: ${{ github.event.after }} | |
| SHORT_SRC_SHA: ${{ steps.short-src-sha.outputs.sha }} | |
| run: | | |
| git config --global user.name "phpstan-bot" | |
| git config --global user.email "ondrej+phpstanbot@mirtes.cz" | |
| git add . | |
| git commit --gpg-sign -m "Updated PHPStan to commit $AFTER_SHA" -m "$INPUT_LOG" --author "phpstan-bot <ondrej+phpstanbot@mirtes.cz>" | |
| lucky_commit "$SHORT_SRC_SHA" | |
| git push | |
| - name: "Commit PHAR - tag" | |
| if: "startsWith(github.ref, 'refs/tags/')" | |
| uses: stefanzweifel/git-auto-commit-action@4a55954c782fc1ea30b9056cd3e7a2b40ca8887d # v7.2.0 | |
| with: | |
| commit_user_name: "phpstan-bot" | |
| commit_user_email: "ondrej+phpstanbot@mirtes.cz" | |
| commit_author: "phpstan-bot <ondrej+phpstanbot@mirtes.cz>" | |
| commit_options: "--gpg-sign" | |
| repository: phpstan-dist | |
| commit_message: "PHPStan ${{github.ref_name}}" | |
| tagging_message: ${{github.ref_name}} |