Skip to content

Check the #[\Override] attribute on class constants and enum cases #33744

Check the #[\Override] attribute on class constants and enum cases

Check the #[\Override] attribute on class constants and enum cases #33744

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