Skip to content

Update github-actions to v26 #33818

Update github-actions to v26

Update github-actions to v26 #33818

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.
# None at the moment: the Linux ZTS binaries retired for 8.3-8.5 are built
# again (see turbo-compile).
TURBO_RETIRED_BINARIES: ""
# 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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
fetch-depth: 0
- name: "Install PHP"
uses: "shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962" # 2.40.0
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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
fetch-depth: 0 # git log over turbo-ext/src needs full history
- name: "Install PHP"
uses: "shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962" # 2.40.0
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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
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@eb7c497e18156a6bbabfef1d3a82760b9eda3962" # 2.40.0
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 building a platform's profile-guided core takes up to
# 12 minutes. For each core, 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 turbo-compile-core leg skips the build altogether: it runs on
# ubuntu-latest, outside any build container, and only carries that core
# over into this run. The per-version extensions always compile (a
# minute each) against it, then go through the version check and the
# differential tests of the turbo-differential jobs like any build —
# they compare the native side with the PHP side of this commit, which
# may have changed. An extension compiled against 8.6 prerelease headers
# that moved on since fails its own leg, not the reused core.
# 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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
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-core:
name: "Compile Turbo Extension Core"
# The distributed binaries are a shared core per platform plus a thin
# extension per PHP version (turbo-ext/README.md, "Shared core"). The
# core is everything but the four version-specific sources: it compiles
# to the same code against every supported PHP's headers
# (turbo-shared-core-gate proves it), so it is built once per platform,
# here, profile-guided against one PHP version (matrix php-version), and
# every turbo-compile leg of the platform links its extension against it.
# A leg whose core turbo-origins found in an earlier run builds nothing:
# it runs on ubuntu-latest without a container and carries that core
# over (runs-on and container cannot read env, hence the lookup of
# ORIGIN_RUN_ID repeated in both).
runs-on: ${{ fromJSON(needs.turbo-origins.outputs.origins || '{}')[format('phpstan_turbo_core-{0}', matrix.target.name)] && 'ubuntu-latest' || matrix.target.runs-on }}
# Container legs pin the binaries' userland floor independently of the
# runner image: the gnu legs run in the prebuilt ubuntu:22.04-based
# image (.github/turbo-build/Dockerfile, published to GHCR by
# turbo-build-image.yml) — the glibc symbol floor stays at 2.34 (RHEL 9,
# Ubuntu 22.04+, Debian 12+) and the jobs run no apt at all — and
# alpine:3.24 builds the musl variant. The linux-musl-arm64 core is in
# turbo-compile-core-musl-arm64 (see turbo-compile-musl-arm64 for why).
container: ${{ !(fromJSON(needs.turbo-origins.outputs.origins || '{}')[format('phpstan_turbo_core-{0}', matrix.target.name)]) && (matrix.target.family == 'gnu' && format('ghcr.io/phpstan/turbo-build:gnu-php{0}', matrix.target.php-version) || matrix.target.container) || '' }}
needs: turbo-origins
timeout-minutes: 30
permissions:
contents: read # actions/checkout of this repository
actions: read # downloads a reused core from an earlier run, see turbo-origins
env:
PHP_MINOR: ${{ matrix.target.php-version }}
PHP_ZTS: "0"
TURBO_ARTIFACT: "phpstan_turbo_core-${{ matrix.target.name }}"
ORIGIN_RUN_ID: ${{ fromJSON(needs.turbo-origins.outputs.origins || '{}')[format('phpstan_turbo_core-{0}', matrix.target.name)] }}
strategy:
fail-fast: false
matrix:
# php-version: the PHP the core is compiled and profiled against —
# any supported one gives the same code (the gate); the newest
# stable one runs the training workload the way most users will
target:
- name: "linux-gnu-x86_64"
runs-on: "ubuntu-latest"
family: "gnu"
php-version: "8.5"
- name: "linux-gnu-arm64"
runs-on: "ubuntu-24.04-arm"
family: "gnu"
php-version: "8.5"
- name: "linux-musl-x86_64"
runs-on: "ubuntu-latest"
container: "alpine:3.24"
family: "musl"
php-version: "8.5"
# arm64 (Apple Silicon) only: setup-php dropped Intel macOS, and
# TurboExtensionSelector resolves Darwin to macos-arm64 only when
# php_uname('m') says arm64.
- name: "macos-arm64"
runs-on: "macos-latest"
family: "macos"
php-version: "8.5"
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 core 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 core"
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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
# The Makefile bakes the 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"
- 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@eb7c497e18156a6bbabfef1d3a82760b9eda3962" # 2.40.0
with:
coverage: "none"
php-version: "${{ matrix.target.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 the core with strict warnings (profile-guided)"
if: env.ORIGIN_RUN_ID == ''
working-directory: "turbo-ext"
# `make SPLIT=1 pgo`: an instrumented core and extension, a training
# run of PHPStan over its own sources with them loaded, then the
# final build using the recorded profile — the same three stages
# locally, so a profile-guided core can be reproduced outside CI.
# The strict set itself lives in turbo-ext/Makefile
# (STRICT_WARN_FLAGS), which documents why each exemption is there.
run: |
make SPLIT=1 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. The symbol tables are a quarter of the binary; see `strip`
# in the Makefile.
- name: "Strip symbol tables"
if: env.ORIGIN_RUN_ID == ''
working-directory: "turbo-ext"
run: |
ls -l phpstan_turbo_core.so phpstan_turbo.so
make SPLIT=1 strip
ls -l phpstan_turbo_core.so phpstan_turbo.so
# On arm64 AMFI refuses to map unsigned code, so the binary must carry
# at least an ad-hoc signature. 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 core (macOS)"
if: env.ORIGIN_RUN_ID == '' && matrix.target.family == 'macos'
working-directory: "turbo-ext"
run: |
codesign --force --sign - phpstan_turbo_core.so phpstan_turbo.so
codesign --verify --verbose=2 phpstan_turbo_core.so
# The extension the training run loaded proves the core loads and
# carries the version the extension expects; a reused core passed
# this in the run compiling it.
- name: "Verify the core 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 "core + extension report: $REPORTED, enabler expects: $EXPECTED"
[ "$REPORTED" = "$EXPECTED" ]
# A reused core is uploaded again under the same name, so the
# turbo-compile legs find it in this run.
- name: "Upload core artifact"
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ${{ env.TURBO_ARTIFACT }}
path: "turbo-ext/phpstan_turbo_core.so"
if-no-files-found: "error"
# Keeps later runs from reusing the copy above: they take the core
# from the run that compiled it (see find-turbo-origins.sh).
- name: "Mark the core 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-core-musl-arm64:
name: "Compile Turbo Extension Core (musl arm64)"
# The arm64 counterpart of the linux-musl-x86_64 leg of
# turbo-compile-core, driving an Alpine container through docker exec
# (see turbo-compile-musl-arm64 for why it cannot be a container leg).
runs-on: ${{ fromJSON(needs.turbo-origins.outputs.origins || '{}')['phpstan_turbo_core-linux-musl-arm64'] && '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 core from an earlier run, see turbo-origins
env:
# the PHP the core is compiled and profiled against, as in turbo-compile-core
PHP_MINOR: "8.5"
PHP_ZTS: "0"
TURBO_ARTIFACT: "phpstan_turbo_core-linux-musl-arm64"
ORIGIN_RUN_ID: ${{ fromJSON(needs.turbo-origins.outputs.origins || '{}')['phpstan_turbo_core-linux-musl-arm64'] }}
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-core.
- name: "Download the core 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 core"
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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
fetch-depth: 0 # the version is baked from git log over turbo-ext/src
- name: "Start the Alpine build container"
if: env.ORIGIN_RUN_ID == ''
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:3.24 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".
- 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.
- 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 the core with strict warnings (profile-guided)"
if: env.ORIGIN_RUN_ID == ''
run: |
docker exec -i -w /work/turbo-ext alpine-build sh -e <<'EOF'
make SPLIT=1 pgo WARN_FLAGS="$(make -s print-warn-flags)" -j"$(getconf _NPROCESSORS_ONLN 2>/dev/null || nproc)"
make SPLIT=1 strip
ls -l phpstan_turbo_core.so phpstan_turbo.so
EOF
- name: "Verify the core 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 "core + extension report: $REPORTED, enabler expects: $EXPECTED"
[ "$REPORTED" = "$EXPECTED" ]
EOF
- name: "Upload core artifact"
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ${{ env.TURBO_ARTIFACT }}
path: "turbo-ext/phpstan_turbo_core.so"
if-no-files-found: "error"
- name: "Mark the core 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:
name: "Compile Turbo Extension"
# The extension of one PHP version: the four version-specific sources
# (the Makefile's SHIM_SOURCES) compiled against that version's headers
# and linked against the platform's core from turbo-compile-core. A
# minute's work, so it always runs; the core is what earlier runs'
# builds are reused for. The artifact carries the core next to the
# extension — the tests of the turbo-differential jobs load it from
# there, and the commit job checks that every leg of a platform carries
# the same core.
runs-on: ${{ matrix.target.runs-on }}
# Container legs pin the binaries' userland floor independently of the
# runner image, like turbo-compile-core. Every Linux target has a ZTS
# leg (ts: zts, artifact suffix -zts) next to the NTS one for every
# minor: distros, ppa:ondrej/php, sury, Remi and setup-php ship an NTS
# CLI, while the official Docker images build every 8.6+ variant
# thread-safe, cli and alpine included (docker-library/php#1686), and
# their php:*-zts tags (the base of FrankenPHP) do so for older minors.
# The thread-safe extension is only the per-version shim — it links
# against the platform's one core, which never touches TSRM
# (turbo-shared-core-gate compares the two builds too) — so a ZTS leg
# compiles only those few sources. ondrej/php packages no thread-safe
# PHP, so the gnu-php<minor>-zts images build the minor's newest
# official tarball (build-php.sh; so does gnu-php8.6, a prerelease), and
# the musl ZTS legs build it in alpine:3.24, since Alpine packages no
# thread-safe PHP either. Alpine's NTS php86 packages are
# currently only in edge/testing.
container: ${{ matrix.target.family == 'gnu' && format('ghcr.io/phpstan/turbo-build:gnu-php{0}{1}', matrix.php-version, matrix.ts == 'zts' && '-zts' || '') || matrix.target.family == 'musl' && matrix.php-version == '8.6' && matrix.ts != 'zts' && 'alpine:edge' || matrix.target.container || '' }}
needs: turbo-compile-core
# one platform's failed core must not skip every platform's legs: a leg
# whose core is missing fails at its download instead
if: ${{ !cancelled() }}
timeout-minutes: 20
env:
PHP_MINOR: ${{ matrix.php-version }}
PHP_ZTS: ${{ matrix.ts == 'zts' && '1' || '0' }}
TURBO_ARTIFACT: "phpstan_turbo-${{ matrix.target.name }}-php${{ matrix.php-version }}${{ matrix.ts == 'zts' && '-zts' || '' }}"
strategy:
fail-fast: false
matrix:
php-version: ["8.3", "8.4", "8.5", "8.6"]
# the musl arm64 legs are in turbo-compile-musl-arm64
target:
- name: "linux-gnu-x86_64"
runs-on: "ubuntu-latest"
family: "gnu"
- name: "linux-gnu-arm64"
runs-on: "ubuntu-24.04-arm"
family: "gnu"
- name: "linux-musl-x86_64"
runs-on: "ubuntu-latest"
container: "alpine:3.24"
family: "musl"
# arm64 (Apple Silicon) only, like turbo-compile-core
- name: "macos-arm64"
runs-on: "macos-latest"
family: "macos"
ts: ["nts", "zts"]
# Homebrew and setup-php ship an NTS PHP on macOS, and no
# thread-safe distribution is common enough there to build for
exclude:
- target:
name: "macos-arm64"
runs-on: "macos-latest"
family: "macos"
ts: "zts"
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
# 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: matrix.target.family == 'musl'
run: apk add --no-cache git
- name: "Checkout"
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
# the version is baked from git log over turbo-ext/src, see
# turbo-compile-core
fetch-depth: 0
# see turbo-compile-core
- name: "Trust the checkout despite the container/host uid mismatch"
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: matrix.target.family == 'musl'
run: sh .github/scripts/install-alpine-php.sh
- name: "Install PHP (macOS)"
if: matrix.target.family == 'macos'
uses: "shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962" # 2.40.0
with:
coverage: "none"
php-version: "${{ matrix.php-version }}"
- name: "Download the platform's core"
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: "phpstan_turbo_core-${{ matrix.target.name }}"
path: "turbo-ext"
# The strict set lives in turbo-ext/Makefile (STRICT_WARN_FLAGS).
- name: "Compile the extension with strict warnings"
working-directory: "turbo-ext"
run: |
make SPLIT=1 extension WARN_FLAGS="$(make -s print-warn-flags)" -j"$(getconf _NPROCESSORS_ONLN 2>/dev/null || nproc)"
# Before signing and the upload; a plain `make strip` leaves the core
# (already stripped and signed by its own job) untouched.
- name: "Strip symbol tables"
working-directory: "turbo-ext"
run: |
ls -l phpstan_turbo.so phpstan_turbo_core.so
make strip
ls -l phpstan_turbo.so phpstan_turbo_core.so
# see turbo-compile-core
- name: "Ad-hoc sign the extension (macOS)"
if: 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 — or paired with a
# core of another version, which leaves it reporting "core-mismatch" —
# would ship inactive.
- 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" ]
# 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" ]
- name: "Upload extension artifact"
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ${{ env.TURBO_ARTIFACT }}
path: |
turbo-ext/phpstan_turbo.so
turbo-ext/phpstan_turbo_core.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 commands run inside a long-lived Alpine
# container via docker exec — native arm64, no QEMU.
runs-on: "ubuntu-26.04-arm"
needs: turbo-compile-core-musl-arm64
if: ${{ !cancelled() }}
timeout-minutes: 20
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' || '' }}"
strategy:
fail-fast: false
matrix:
php-version: ["8.3", "8.4", "8.5", "8.6"]
# thread-safe for every minor too, see turbo-compile
ts: ["nts", "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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
fetch-depth: 0 # the version is baked from git log over turbo-ext/src
- name: "Download the platform's core"
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: "phpstan_turbo_core-linux-musl-arm64"
path: "turbo-ext"
- name: "Start the Alpine build container"
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 \
"$ALPINE_IMAGE" sleep 7200
- name: "Install PHP and build tools"
run: docker exec alpine-build sh .github/scripts/install-alpine-php.sh
# see turbo-compile-core-musl-arm64
- name: "Trust the checkout despite the container/host uid mismatch"
run: docker exec alpine-build git config --global --add safe.directory /work
- name: "Compile the extension with strict warnings"
run: |
docker exec -i -w /work/turbo-ext alpine-build sh -e <<'EOF'
make SPLIT=1 extension WARN_FLAGS="$(make -s print-warn-flags)" -j"$(getconf _NPROCESSORS_ONLN 2>/dev/null || nproc)"
make strip
ls -l phpstan_turbo.so phpstan_turbo_core.so
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" ]
[ "$(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
turbo-ext/phpstan_turbo_core.so
if-no-files-found: "error"
turbo-shared-core-gate:
name: "Turbo Shared Core Gate"
# The core every platform ships is compiled against one PHP version's
# headers and runs on all of them, so the sources it is built from must
# compile to identical code against each supported version's — a source
# reading a member whose offset moved, a constant whose value changed or
# a function whose signature did would otherwise misbehave on the other
# versions only, on the paths that touch it. Compiles every source
# against each version's (and the 8.6 thread-safe) headers in the CI
# build images with clang, and fails on any difference outside the
# version-specific sources (turbo-ext/bin/shared-core/check-linux.sh).
runs-on: "ubuntu-latest"
timeout-minutes: 40
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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: "Compare the shared code across the supported PHP versions"
env:
SHARED_CORE_WORK_DIR: ${{ runner.temp }}/shared-core
run: |
mkdir -p "$SHARED_CORE_WORK_DIR"
bash turbo-ext/bin/shared-core/check-linux.sh
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. The 8.5 php:*-zts pair (the base of
# FrankenPHP's images) does the same for the older minors' ZTS
# binaries, which are compiled against our own build of the tarball. 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' || contains(matrix.image, 'alpine')) && 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.5-zts-bookworm"
- "php:8.5-zts-alpine"
- "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.5-zts-bookworm"
php-version: "8.5"
binary: "linux-gnu-x86_64-php8.5-zts"
- image: "php:8.5-zts-alpine"
php-version: "8.5"
binary: "linux-musl-x86_64-php8.5-zts"
- 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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
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@eb7c497e18156a6bbabfef1d3a82760b9eda3962" # 2.40.0
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, bash and make to the 8.6 prerelease and Alpine images"
if: matrix.php-version == '8.6' || contains(matrix.image, 'alpine')
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; \
fi \
&& if ! php -m | grep -qx mbstring; then \
if command -v apk > /dev/null; then \
apk add --no-cache 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; \
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-core-windows:
name: "Compile Turbo Extension Core (Windows)"
# The Windows core, once per thread-safety flavour: a DLL imports the
# engine from php8.dll or php8ts.dll by name, so the two cannot share
# one. Built against the oldest supported minor's devel pack with its
# toolset (VS16, linker 14.2x): PHP's loader only checks the toolset of
# the extension DLL it loads (turbo-compile-windows builds each with its
# minor's), and a core linked with an older toolset runs on every newer
# VC runtime.
runs-on: ${{ fromJSON(needs.turbo-origins.outputs.origins || '{}')[format('phpstan_turbo_core-windows-x86_64{0}', matrix.ts == 'zts' && '-zts' || '')] && 'ubuntu-latest' || 'windows-2022' }}
needs: turbo-origins
timeout-minutes: 40
permissions:
contents: read # actions/checkout of this repository
actions: read # downloads a reused core 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_core-windows-x86_64${{ matrix.ts == 'zts' && '-zts' || '' }}"
ORIGIN_RUN_ID: ${{ fromJSON(needs.turbo-origins.outputs.origins || '{}')[format('phpstan_turbo_core-windows-x86_64{0}', matrix.ts == 'zts' && '-zts' || '')] }}
strategy:
fail-fast: false
matrix:
ts: ["nts", "zts"]
php-version: ["8.3"]
vs: ["vs16"]
toolset: ["14.29"]
steps:
# See the same two steps in turbo-compile-core; this leg runs on
# ubuntu-latest.
- name: "Download the core 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 core"
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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
fetch-depth: 0 # the version is baked from git log over turbo-ext/src
- name: "Install PHP"
if: env.ORIGIN_RUN_ID == ''
uses: "shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962" # 2.40.0
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"
if: env.ORIGIN_RUN_ID == ''
shell: bash
env:
MATRIX_TS: ${{ matrix.ts }}
MATRIX_VS: ${{ matrix.vs }}
run: bash .github/scripts/download-php-windows-devel.sh
- name: "Set up MSVC environment"
if: env.ORIGIN_RUN_ID == ''
uses: ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756 # v1.13.0
with:
arch: x64
toolset: ${{ matrix.toolset }}
- name: "Build the core"
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=core
if errorlevel 1 exit /b 1
nmake
# the DLL and its import library, which turbo-compile-windows links
# each extension against
- name: "Collect the core"
if: env.ORIGIN_RUN_ID == ''
shell: bash
env:
MATRIX_TS: ${{ matrix.ts }}
run: |
CORE=$([ "$MATRIX_TS" = "zts" ] && echo phpstan_turbo_core-zts || echo phpstan_turbo_core)
mkdir -p core-out
DLL=$(find turbo-ext -name "$CORE.dll" | head -1)
LIB=$(find turbo-ext -name "$CORE.lib" | head -1)
[ -n "$DLL" ] && [ -n "$LIB" ]
cp "$DLL" "$LIB" core-out/
ls -l core-out
echo "CORE_DIR=core-out" >> "$GITHUB_ENV"
- name: "Point the upload at the reused core"
if: env.ORIGIN_RUN_ID != ''
run: echo "CORE_DIR=turbo-ext" >> "$GITHUB_ENV"
- name: "Upload core artifact"
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ${{ env.TURBO_ARTIFACT }}
path: "${{ env.CORE_DIR }}/phpstan_turbo_core*"
if-no-files-found: "error"
- name: "Mark the core 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-windows:
name: "Compile Turbo Extension (Windows)"
# The extension DLL of one PHP version, linked against the core of
# turbo-compile-core-windows (its import library) and delay-loading it
# from its own directory at run time (main.cpp, config.w32).
# 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.
runs-on: ${{ matrix.php-version == '8.6' && 'windows-2025-vs2026' || 'windows-2022' }}
needs: turbo-compile-core-windows
# see turbo-compile
if: ${{ !cancelled() }}
timeout-minutes: 30
env:
PHP_SDK_COMMIT: "c73faaf1cce914e2fc04da5587132f8f425996af" # php-sdk-2.7.1, see turbo-compile-core-windows
TURBO_ARTIFACT: "phpstan_turbo-windows-x86_64-php${{ 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:
- name: "Checkout"
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
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@eb7c497e18156a6bbabfef1d3a82760b9eda3962" # 2.40.0
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.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"
shell: bash
env:
MATRIX_TS: ${{ matrix.ts }}
MATRIX_VS: ${{ matrix.vs }}
run: bash .github/scripts/download-php-windows-devel.sh
# the DLL and its import library, into the extension directory where
# config.w32 expects the library
- name: "Download the core"
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: "phpstan_turbo_core-windows-x86_64${{ matrix.ts == 'zts' && '-zts' || '' }}"
path: "turbo-ext"
- name: "Set up MSVC environment"
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"
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=extension
if errorlevel 1 exit /b 1
nmake
# See the same step in turbo-compile. The extension loads its core
# from its own directory, so the core goes next to the built DLL first.
- name: "Verify the built extension reports the expected version"
shell: bash
env:
MATRIX_TS: ${{ matrix.ts }}
run: |
CORE=$([ "$MATRIX_TS" = "zts" ] && echo phpstan_turbo_core-zts.dll || echo phpstan_turbo_core.dll)
DLL=$(find turbo-ext -name "php_phpstan_turbo.dll" | head -1)
[ -n "$DLL" ]
mkdir -p ext-out
cp "$DLL" ext-out/php_phpstan_turbo.dll
cp "turbo-ext/$CORE" ext-out/
REPORTED="$(php -d extension="$(cygpath -w "$PWD/ext-out/php_phpstan_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" ]
- name: "Upload extension artifact"
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ${{ env.TURBO_ARTIFACT }}
path: "ext-out/*.dll"
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
# before the download: checkout empties the workspace
- name: "Checkout"
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
sparse-checkout: .github/scripts
- name: "Download extension artifacts"
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: "phpstan_turbo-*"
path: "turbo-artifacts"
- name: "Arrange the distribution layout"
# each artifact's extension and the platform's core, mapped by
# .github/scripts/turbo-dist-layout.sh (which also refuses legs of
# one platform carrying different cores) — 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: |
bash .github/scripts/turbo-dist-layout.sh turbo-artifacts > layout.tsv
while IFS=$'\t' read -r file dest; do
install -D -m 644 "$file" "turbo-ext-dist/$dest"
done < layout.tsv
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 and ZTS 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; the ZTS legs so as not to depend on setup-php's thread-safe
# Linux builds.
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"]
# macOS ships no ZTS binary (see turbo-compile)
exclude:
- 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"
ts: "nts"
container: "ghcr.io/phpstan/turbo-build:gnu-php8.6"
- target: "linux-gnu-arm64"
php-version: "8.6"
ts: "nts"
container: "ghcr.io/phpstan/turbo-build:gnu-php8.6"
# the linux-gnu ZTS legs, in the image the binary is compiled in
# (see above); the official php:*-zts images get turbo-docker-run
- target: "linux-gnu-x86_64"
php-version: "8.3"
ts: "zts"
runs-on: "ubuntu-latest"
container: "ghcr.io/phpstan/turbo-build:gnu-php8.3-zts"
- target: "linux-gnu-arm64"
php-version: "8.3"
ts: "zts"
runs-on: "ubuntu-24.04-arm"
container: "ghcr.io/phpstan/turbo-build:gnu-php8.3-zts"
- target: "linux-gnu-x86_64"
php-version: "8.4"
ts: "zts"
runs-on: "ubuntu-latest"
container: "ghcr.io/phpstan/turbo-build:gnu-php8.4-zts"
- target: "linux-gnu-arm64"
php-version: "8.4"
ts: "zts"
runs-on: "ubuntu-24.04-arm"
container: "ghcr.io/phpstan/turbo-build:gnu-php8.4-zts"
- target: "linux-gnu-x86_64"
php-version: "8.5"
ts: "zts"
runs-on: "ubuntu-latest"
container: "ghcr.io/phpstan/turbo-build:gnu-php8.5-zts"
- target: "linux-gnu-arm64"
php-version: "8.5"
ts: "zts"
runs-on: "ubuntu-24.04-arm"
container: "ghcr.io/phpstan/turbo-build:gnu-php8.5-zts"
- 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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: "Install PHP"
if: ${{ !matrix.container }}
uses: "shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962" # 2.40.0
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"]
# thread-safe for every minor too, see turbo-compile
ts: ["nts", "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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
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"]
# The Linux ZTS binaries run the same checks in turbo-docker-run, in
# the official thread-safe images, instead of under setup-php.
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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: "Install PHP"
uses: "shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962" # 2.40.0
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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
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@eb7c497e18156a6bbabfef1d3a82760b9eda3962" # 2.40.0
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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: "Install PHP"
uses: "shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962" # 2.40.0
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"
cp tmp/turbo-artifact/phpstan_turbo_core*.dll tmp/turbo-ext/windows-x86_64/
- 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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
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@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: 24
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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
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@eb7c497e18156a6bbabfef1d3a82760b9eda3962" # 2.40.0
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-shared-core-gate
- 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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
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. The layout script maps each artifact file
# to its dist path (extensions and the platforms' cores).
bash phpstan-src/.github/scripts/turbo-dist-layout.sh turbo-artifacts > turbo-layout.tsv
while IFS=$'\t' read -r _ dest; do
if [[ ! -f "phpstan-dist/turbo-ext/$dest" ]]; then
echo "missing from dist: phpstan-dist/turbo-ext/$dest"
RESULT=different
fi
done < turbo-layout.tsv
# 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
# every binary the dist carries — extensions and cores alike —
# needs a freshly built replacement in the layout
for dist_file in phpstan-dist/turbo-ext/*/phpstan_turbo*; do
[ -f "$dist_file" ] || continue
rel="${dist_file#phpstan-dist/turbo-ext/}"
if ! cut -f2 turbo-layout.tsv | grep -qxF "$rel"; then
echo "::error::$dist_file has no freshly built replacement in the downloaded artifacts - refusing to bump turbo-ext/.version over a stale binary."
MISSING=yes
fi
done
if [[ -n "$MISSING" ]]; then
exit 1
fi
fi
while IFS=$'\t' read -r file rel; do
dest="phpstan-dist/turbo-ext/$rel"
if [[ "$VERSION_CHANGED" == "yes" || ! -f "$dest" ]]; then
install -D -m 644 "$file" "$dest"
fi
done < turbo-layout.tsv
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}}