Skip to content

Narrow foreach body to non-empty regardless of polluteScopeWithAlwaysIterableForeach #31134

Narrow foreach body to non-empty regardless of polluteScopeWithAlwaysIterableForeach

Narrow foreach body to non-empty regardless of polluteScopeWithAlwaysIterableForeach #31134

Workflow file for this run

# 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}}