Narrow foreach body to non-empty regardless of polluteScopeWithAlwaysIterableForeach #31134
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.2.x" | |
| tags: | |
| - '2.2.*' | |
| concurrency: | |
| group: phar-${{ github.ref }} # will be canceled on subsequent pushes in both branches and pull requests | |
| cancel-in-progress: true | |
| env: | |
| # 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.8.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" | |
| jobs: | |
| compiler-tests: | |
| name: "Compiler Tests" | |
| runs-on: "ubuntu-latest" | |
| timeout-minutes: 60 | |
| 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: | |
| 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" | |
| - 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.2.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.2.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 | |
| - 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 until byte-identical, run the full suite, then bump SUPPORTED_PHP_PARSER_VERSION here and the extension version pin." | |
| exit 1 | |
| fi | |
| - 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 | |
| - name: "Smoke test (differential: native vs PHP implementations)" | |
| 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: "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-compile: | |
| name: "Compile Turbo Extension" | |
| runs-on: ${{ 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. The ZTS legs (for | |
| # hosts with a thread-safe PHP, like PMMP's bundled build) run directly | |
| # on ubuntu-22.04 runners — same glibc floor — because no distro | |
| # packages ZTS PHP (neither ppa:ondrej/php nor Alpine); setup-php | |
| # provides thread-safe builds. No musl ZTS for the same packaging | |
| # reason. | |
| container: ${{ matrix.target.family == 'gnu' && format('ghcr.io/phpstan/turbo-build:gnu-php{0}', matrix.php-version) || matrix.target.container }} | |
| timeout-minutes: 30 | |
| env: | |
| PHP_MINOR: ${{ matrix.php-version }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| php-version: ["8.3", "8.4", "8.5"] | |
| 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" | |
| - name: "linux-gnu-x86_64" | |
| runs-on: "ubuntu-22.04" | |
| family: "zts" | |
| artifact: "phpstan_turbo" | |
| suffix: "-zts" | |
| - name: "linux-gnu-arm64" | |
| runs-on: "ubuntu-22.04-arm" | |
| family: "zts" | |
| artifact: "phpstan_turbo" | |
| suffix: "-zts" | |
| # The macOS slices are merged into one universal binary (and ad-hoc | |
| # signed) by the turbo-macos-universal job below, so their artifacts | |
| # carry the turbo-slice prefix that the phpstan_turbo-* download | |
| # patterns in the aggregate and commit jobs do not match. | |
| - name: "macos-x86_64" | |
| runs-on: "macos-15-intel" # the last x86_64 macOS image, available until August 2027 | |
| family: "macos" | |
| artifact: "turbo-slice" | |
| - name: "macos-arm64" | |
| runs-on: "macos-latest" | |
| family: "macos" | |
| artifact: "turbo-slice" | |
| steps: | |
| - name: Harden the runner (Audit all outbound calls) | |
| if: matrix.target.family == 'macos' | |
| uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 | |
| with: | |
| egress-policy: audit | |
| # The Alpine leg installs PHP and tooling before checkout: | |
| # actions/checkout needs git inside the container, and the build needs | |
| # php/php-config symlinks pointing at the matrix PHP version. The gnu | |
| # legs need no install step — their prebuilt turbo-build image bakes | |
| # all of this in. | |
| - name: "Install PHP and build tools (Alpine container)" | |
| if: matrix.target.family == 'musl' | |
| run: | | |
| V="$(echo "$PHP_MINOR" | tr -d .)" | |
| apk add --no-cache bash curl git make g++ musl-dev linux-headers patch tar zstd \ | |
| "php$V" "php$V-dev" "php$V-ctype" "php$V-curl" "php$V-mbstring" \ | |
| "php$V-tokenizer" "php$V-iconv" "php$V-openssl" "php$V-phar" \ | |
| "php$V-dom" "php$V-xml" "php$V-xmlwriter" "php$V-simplexml" | |
| ln -sf "/usr/bin/php$V" /usr/local/bin/php | |
| ln -sf "/usr/bin/php-config$V" /usr/local/bin/php-config | |
| - name: "Checkout" | |
| 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" | |
| run: git config --global --add safe.directory "$GITHUB_WORKSPACE" | |
| - name: "Install PHP (macOS)" | |
| if: matrix.target.family == 'macos' | |
| uses: "shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240" # v2.37.2 | |
| with: | |
| coverage: "none" | |
| php-version: "${{ matrix.php-version }}" | |
| - name: "Install PHP (ZTS runner)" | |
| if: matrix.target.family == 'zts' | |
| uses: "shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240" # v2.37.2 | |
| env: | |
| phpts: "ts" | |
| with: | |
| coverage: "none" | |
| php-version: "${{ matrix.php-version }}" | |
| # Guards against setup-php silently falling back to an NTS build, | |
| # which would ship an NTS binary under the -zts name. | |
| - name: "Verify the interpreter is thread-safe" | |
| if: matrix.target.family == 'zts' | |
| run: "php -r 'exit((bool) PHP_ZTS ? 0 : 1);'" | |
| - name: "Compile phpstan_turbo with strict warnings" | |
| working-directory: "turbo-ext" | |
| # Exemptions, each caused by third-party macro expansions, not our code: | |
| # -Wno-assume: zend's parameter-parsing macros expand __builtin_assume | |
| # with (potential) side effects | |
| # -Wno-unused-parameter: PHP_METHOD's fixed signature | |
| # (execute_data/return_value are not used by every method) | |
| # -Wno-unicode: zend arginfo macros stringify namespaced class names; | |
| # clang lexes the \N in "PhpParser\NodeVisitor" as a universal | |
| # character name (GCC ignores the unknown -Wno- flag) | |
| run: | | |
| make WARN_FLAGS="-Wall -Wextra -Werror -Wno-assume -Wno-unused-parameter -Wno-unicode" -j"$(getconf _NPROCESSORS_ONLN 2>/dev/null || nproc)" | |
| - name: "Verify the built extension reports the expected version" | |
| 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" ] | |
| - uses: "ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda" # v4.0.0 | |
| if: matrix.target.family == 'macos' | |
| - name: "Install Composer dependencies (pinned composer)" | |
| if: matrix.target.family != 'macos' | |
| # checksum-pinned composer, run under the matrix PHP so platform | |
| # requirements are validated against the interpreter the tests below | |
| # run on. COMPOSER_HOME is isolated: on the runner (ZTS) legs, | |
| # setup-php seeds ~/.composer/auth.json with the workflow's GitHub | |
| # token, which this pinned composer rejects at startup ("github oauth | |
| # token contains invalid characters") — everything here comes from | |
| # packagist dists, no auth needed. | |
| run: | | |
| 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 | |
| - name: "Smoke test (differential: native vs PHP implementations)" | |
| run: php -d extension="$PWD/turbo-ext/phpstan_turbo.so" turbo-ext/tests/smoke.php | |
| - name: "Arena smoke test (cross-process shared-memory records)" | |
| run: php -d extension="$PWD/turbo-ext/phpstan_turbo.so" turbo-ext/tests/arena-smoke.php | |
| - name: "Signature parity (reflect native classes against the PHP twins)" | |
| run: php -d extension="$PWD/turbo-ext/phpstan_turbo.so" turbo-ext/tests/signature-parity.php | |
| - name: "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: "Parser corpus (differential: native vs PHP ASTs must be byte-identical)" | |
| run: php -d extension="$PWD/turbo-ext/phpstan_turbo.so" -d memory_limit=4G turbo-ext/tests/parser-corpus.php | |
| - name: "Upload extension artifact" | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: "${{ matrix.target.artifact }}-${{ matrix.target.name }}-php${{ matrix.php-version }}${{ matrix.target.suffix }}" | |
| path: "turbo-ext/phpstan_turbo.so" | |
| 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 and test commands run inside a long-lived | |
| # Alpine container via docker exec — native arm64, no QEMU. The steps | |
| # mirror the musl leg of turbo-compile one for one. | |
| runs-on: "ubuntu-24.04-arm" | |
| timeout-minutes: 30 | |
| env: | |
| PHP_MINOR: ${{ matrix.php-version }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| php-version: ["8.3", "8.4", "8.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 | |
| # 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" | |
| run: | | |
| docker run -d --name alpine-build \ | |
| -v "$PWD:/work" -w /work \ | |
| -e PHP_MINOR -e COMPOSER_VERSION -e COMPOSER_PHAR_SHA256 \ | |
| alpine:3.24 sleep 7200 | |
| - name: "Install PHP and build tools" | |
| run: | | |
| docker exec -i alpine-build sh -e <<'EOF' | |
| V="$(echo "$PHP_MINOR" | tr -d .)" | |
| apk add --no-cache bash curl git make g++ musl-dev linux-headers patch tar zstd \ | |
| "php$V" "php$V-dev" "php$V-ctype" "php$V-curl" "php$V-mbstring" \ | |
| "php$V-tokenizer" "php$V-iconv" "php$V-openssl" "php$V-phar" \ | |
| "php$V-dom" "php$V-xml" "php$V-xmlwriter" "php$V-simplexml" | |
| ln -sf "/usr/bin/php$V" /usr/local/bin/php | |
| ln -sf "/usr/bin/php-config$V" /usr/local/bin/php-config | |
| EOF | |
| # 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" | |
| run: docker exec alpine-build git config --global --add safe.directory /work | |
| - name: "Compile phpstan_turbo with strict warnings" | |
| run: | | |
| docker exec -i -w /work/turbo-ext alpine-build sh -e <<'EOF' | |
| make WARN_FLAGS="-Wall -Wextra -Werror -Wno-assume -Wno-unused-parameter -Wno-unicode" -j"$(getconf _NPROCESSORS_ONLN 2>/dev/null || nproc)" | |
| EOF | |
| - name: "Verify the built extension reports the expected version" | |
| 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" ] | |
| EOF | |
| - name: "Install Composer dependencies (pinned composer)" | |
| 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: "Smoke test (differential: native vs PHP implementations)" | |
| run: docker exec alpine-build 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-build php -d extension=/work/turbo-ext/phpstan_turbo.so turbo-ext/tests/arena-smoke.php | |
| - name: "Signature parity (reflect native classes against the PHP twins)" | |
| run: docker exec alpine-build php -d extension=/work/turbo-ext/phpstan_turbo.so turbo-ext/tests/signature-parity.php | |
| - name: "Parser actions are generated (regenerate and diff)" | |
| run: | | |
| docker exec -i alpine-build sh -e <<'EOF' | |
| php turbo-ext/bin/generate-parser-actions.php | |
| git diff --exit-code turbo-ext/src/parser/ | |
| EOF | |
| - name: "Parser corpus (differential: native vs PHP ASTs must be byte-identical)" | |
| run: docker exec alpine-build php -d extension=/work/turbo-ext/phpstan_turbo.so -d memory_limit=4G turbo-ext/tests/parser-corpus.php | |
| - name: "Upload extension artifact" | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: "phpstan_turbo-linux-musl-arm64-php${{ matrix.php-version }}" | |
| path: "turbo-ext/phpstan_turbo.so" | |
| if-no-files-found: "error" | |
| turbo-macos-universal: | |
| name: "Turbo macOS Universal Binary" | |
| needs: "turbo-compile" | |
| runs-on: "macos-latest" | |
| timeout-minutes: 30 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| php-version: ["8.3", "8.4", "8.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 x86_64 slice" | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: "turbo-slice-macos-x86_64-php${{ matrix.php-version }}" | |
| path: "slice-x86_64" | |
| - name: "Download arm64 slice" | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: "turbo-slice-macos-arm64-php${{ matrix.php-version }}" | |
| path: "slice-arm64" | |
| - name: "Merge the slices into a universal binary" | |
| run: | | |
| lipo -create slice-x86_64/phpstan_turbo.so slice-arm64/phpstan_turbo.so -output phpstan_turbo.so | |
| lipo -info phpstan_turbo.so | |
| # On arm64 AMFI refuses to map unsigned code, so the binary must carry | |
| # at least an ad-hoc signature. lipo preserves the linker's per-slice | |
| # ad-hoc signatures, 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 universal binary" | |
| run: | | |
| codesign --force --sign - phpstan_turbo.so | |
| codesign --verify --verbose=2 phpstan_turbo.so | |
| - name: "Install PHP" | |
| uses: "shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240" # v2.37.2 | |
| with: | |
| coverage: "none" | |
| php-version: "${{ matrix.php-version }}" | |
| - name: "Verify the universal binary loads" | |
| run: php -d extension="$PWD/phpstan_turbo.so" -m | grep phpstan_turbo | |
| - name: "Upload universal binary artifact" | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: "phpstan_turbo-macos-php${{ matrix.php-version }}" | |
| path: "phpstan_turbo.so" | |
| if-no-files-found: "error" | |
| 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. | |
| runs-on: "windows-2022" | |
| timeout-minutes: 40 | |
| 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 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| php-version: ["8.3", "8.4", "8.5"] | |
| 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" | |
| steps: | |
| - name: "Checkout" | |
| 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" | |
| uses: "shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240" # v2.37.2 | |
| env: | |
| phpts: "${{ matrix.ts == 'zts' && 'ts' || 'nts' }}" | |
| with: | |
| coverage: "none" | |
| php-version: "${{ matrix.php-version }}" | |
| - name: "Compute the extension version and download the build tools" | |
| shell: bash | |
| 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" | |
| 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" | |
| uses: ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756 # v1.13.0 | |
| with: | |
| arch: x64 | |
| # empty on the vs17 legs = the image's default (latest) toolset | |
| toolset: ${{ matrix.toolset }} | |
| - name: "Build the extension" | |
| 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 | |
| - name: "Verify the built extension reports the expected version" | |
| shell: bash | |
| run: | | |
| DLL=$(find turbo-ext -name "php_phpstan_turbo.dll" | head -1) | |
| [ -n "$DLL" ] | |
| TURBO_DLL=$(cygpath -w "$PWD/$DLL") | |
| echo "TURBO_DLL=$TURBO_DLL" >> "$GITHUB_ENV" | |
| echo "DLL_PATH=$DLL" >> "$GITHUB_ENV" | |
| 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 "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" ] | |
| - uses: "ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda" # v4.0.0 | |
| - name: "Smoke test (differential: native vs PHP implementations)" | |
| shell: bash | |
| run: php -d extension="$TURBO_DLL" turbo-ext/tests/smoke.php | |
| - name: "Arena smoke test (cross-process shared-memory records)" | |
| shell: bash | |
| run: php -d extension="$TURBO_DLL" turbo-ext/tests/arena-smoke.php | |
| - name: "Signature parity (reflect native classes against the PHP twins)" | |
| shell: bash | |
| run: php -d extension="$TURBO_DLL" turbo-ext/tests/signature-parity.php | |
| - name: "Parser corpus (differential: native vs PHP ASTs must be byte-identical)" | |
| shell: bash | |
| run: php -d extension="$TURBO_DLL" -d memory_limit=4G turbo-ext/tests/parser-corpus.php | |
| - name: "Upload extension artifact" | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: "phpstan_turbo-windows-x86_64-php${{ matrix.php-version }}${{ matrix.ts == 'zts' && '-zts' || '' }}" | |
| path: "${{ env.DLL_PATH }}" | |
| if-no-files-found: "error" | |
| turbo-artifact: | |
| name: "Aggregate Turbo Extension Artifact" | |
| needs: | |
| - turbo-compile | |
| - turbo-compile-musl-arm64 | |
| - turbo-compile-windows | |
| - turbo-macos-universal | |
| 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-run: | |
| name: "Run with Turbo Extension" | |
| needs: | |
| - turbo-compile | |
| - turbo-compile-windows | |
| - turbo-macos-universal | |
| 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"] | |
| ts: ["nts", "zts"] | |
| script: ["make tests", "make phpstan"] | |
| # macOS runs the shipped ad-hoc-signed universal 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" | |
| 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: "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' || '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 | |
| - name: "Verify the extension is active" | |
| run: | | |
| php -r ' | |
| require "src/Turbo/TurboExtensionEnabler.php"; | |
| PHPStan\Turbo\TurboExtensionEnabler::enableIfLoaded(); | |
| require "vendor/autoload.php"; | |
| if (!(new ReflectionClass("PHPStan\\Analyser\\ScopeOps"))->getParentClass() || !(new ReflectionClass("PHPStan\\Analyser\\ScopeOps"))->getParentClass()->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. | |
| run: | | |
| if ! command -v make > /dev/null && [ -e /c/msys64/usr/bin/make.exe ]; then | |
| export PATH="$PATH:/c/msys64/usr/bin" | |
| fi | |
| ${{ matrix.script }} | |
| integration-tests: | |
| if: github.event_name == 'pull_request' | |
| needs: | |
| - compiler-tests | |
| - turbo-artifact | |
| uses: phpstan/phpstan/.github/workflows/integration-tests.yml@2.2.x | |
| with: | |
| ref: 2.2.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.2.x | |
| with: | |
| ref: 2.2.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.2.x | |
| with: | |
| ref: 2.2.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" | |
| 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 | |
| - name: Get base commit SHA | |
| id: base | |
| run: echo "base_sha=${{ github.event.pull_request.base.sha }}" >> "$GITHUB_OUTPUT" | |
| - name: Set up Node.js | |
| uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0 | |
| with: | |
| node-version: 20 | |
| - 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" | |
| run: | | |
| old_checksum=${{ steps.old_checksum.outputs.md5 }} | |
| new_checksum=${{needs.compiler-tests.outputs.checksum}} | |
| [[ "$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 | |
| # 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 | |
| - name: "Install Box dependencies" | |
| uses: "ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda" # v4.0.0 | |
| with: | |
| working-directory: "compiler/box" | |
| - 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.2.x' || startsWith(github.ref, 'refs/tags/'))" | |
| needs: | |
| - compiler-tests | |
| - turbo-version | |
| - turbo-compile | |
| - turbo-compile-musl-arm64 | |
| - turbo-compile-windows | |
| - turbo-macos-universal | |
| 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.2.x | |
| - 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: | |
| fetch-depth: 0 | |
| path: phpstan-src | |
| - name: "Get Git log" | |
| id: git-log | |
| working-directory: phpstan-src | |
| run: | | |
| echo "log<<MESSAGE" >> "$GITHUB_OUTPUT" | |
| git log ${{ steps.previous-commit.outputs.sha }}..${{ github.event.after }} --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 | |
| run: | | |
| checksum="${{needs.compiler-tests.outputs.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 | |
| 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' | |
| run: | | |
| echo ${{needs.compiler-tests.outputs.checksum}} > phpstan-dist/.phar-checksum | |
| echo ${{ github.event.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: | | |
| 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 }} | |
| 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 ${{ github.event.after }}" -m "$INPUT_LOG" --author "phpstan-bot <ondrej+phpstanbot@mirtes.cz>" | |
| lucky_commit ${{ steps.short-src-sha.outputs.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}} |