From 193bb4e5cb62a600273a40509c654cd9b0631292 Mon Sep 17 00:00:00 2001
From: staabm <120441+staabm@users.noreply.github.com>
Date: Mon, 29 Jun 2026 21:21:07 +0000
Subject: [PATCH 1/4] Notify in CI when a slow full analysis ran without a
pre-existing result cache
- Add `ResultCacheManager::resultCacheExists()` to report whether the result
cache file was on disk when PHPStan started, before `restore()` can unlink it.
- Thread that fact through `AnalyseApplication` into a new
`AnalysisResult::resultCacheExisted()` getter (defaults to true so a stale but
present cache that forced a full re-analysis does not trigger the message).
- In `AnalyseCommand`, after a full analysis, print a hint to persist the result
cache directory when: running in CI (detected via `CiDetector`), analysing
full paths (not `--only-files`), the cache file did not exist at start, and the
run took longer than `RESULT_CACHE_CI_NOTIFICATION_ELAPSED_LIMIT` (60s).
- The message is intentionally suppressed when the cache existed but was invalid.
---
.../ResultCache/ResultCacheManager.php | 9 ++
src/Command/AnalyseApplication.php | 2 +
src/Command/AnalyseCommand.php | 30 ++++
src/Command/AnalysisResult.php | 12 ++
.../Command/ResultCacheCiNotificationTest.php | 140 ++++++++++++++++++
5 files changed, 193 insertions(+)
create mode 100644 tests/PHPStan/Command/ResultCacheCiNotificationTest.php
diff --git a/src/Analyser/ResultCache/ResultCacheManager.php b/src/Analyser/ResultCache/ResultCacheManager.php
index e3172812c5b..0d3fb510fce 100644
--- a/src/Analyser/ResultCache/ResultCacheManager.php
+++ b/src/Analyser/ResultCache/ResultCacheManager.php
@@ -117,6 +117,15 @@ public function __construct(
{
}
+ /**
+ * Whether the result cache file was present on disk when PHPStan started.
+ * Distinguishes "the cache never existed" from "the cache existed but was invalid".
+ */
+ public function resultCacheExists(): bool
+ {
+ return is_file($this->cacheFilePath);
+ }
+
/**
* @param string[] $allAnalysedFiles
* @param mixed[]|null $projectConfigArray
diff --git a/src/Command/AnalyseApplication.php b/src/Command/AnalyseApplication.php
index 2cc886eb0f9..7ec2169d4fb 100644
--- a/src/Command/AnalyseApplication.php
+++ b/src/Command/AnalyseApplication.php
@@ -71,6 +71,7 @@ public function analyse(
$fileReplacements = [$insteadOfFile => $tmpFile];
}
$resultCacheManager = $this->resultCacheManagerFactory->create($fileReplacements);
+ $resultCacheExisted = $resultCacheManager->resultCacheExists();
$ignoredErrorHelperResult = $this->ignoredErrorHelper->initialize();
$fileSpecificErrors = [];
@@ -190,6 +191,7 @@ public function analyse(
$isResultCacheUsed,
$changedProjectExtensionFilesOutsideOfAnalysedPaths,
$processedFiles,
+ $resultCacheExisted,
);
}
diff --git a/src/Command/AnalyseCommand.php b/src/Command/AnalyseCommand.php
index d2e39d33003..4a9a901259e 100644
--- a/src/Command/AnalyseCommand.php
+++ b/src/Command/AnalyseCommand.php
@@ -54,6 +54,7 @@
use function is_bool;
use function is_file;
use function is_string;
+use function microtime;
use function pathinfo;
use function rewind;
use function sprintf;
@@ -76,6 +77,8 @@ final class AnalyseCommand extends Command
public const DEFAULT_LEVEL = CommandHelper::DEFAULT_LEVEL;
+ private const RESULT_CACHE_CI_NOTIFICATION_ELAPSED_LIMIT = 60.0;
+
/**
* @param string[] $composerAutoloaderProjectPaths
*/
@@ -494,6 +497,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$analysisResult->isResultCacheUsed(),
$analysisResult->getChangedProjectExtensionFilesOutsideOfAnalysedPaths(),
$analysisResult->getProcessedFiles(),
+ $analysisResult->resultCacheExisted(),
);
$exitCode = $errorFormatter->formatErrors($analysisResult, $inceptionResult->getStdOutput());
@@ -650,6 +654,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int
}
}
+ $this->reportMissingResultCacheInCi($errorOutput, $analysisResult, $onlyFiles);
+
$this->runDiagnoseExtensions($container, $inceptionResult->getErrorOutput(), $analysisResult->getProcessedFiles());
return $inceptionResult->handleReturn(
@@ -659,6 +665,30 @@ protected function execute(InputInterface $input, OutputInterface $output): int
);
}
+ private function reportMissingResultCacheInCi(Output $errorOutput, AnalysisResult $analysisResult, bool $onlyFiles): void
+ {
+ if (
+ $onlyFiles
+ || $analysisResult->isResultCacheUsed()
+ || $analysisResult->resultCacheExisted()
+ ) {
+ return;
+ }
+
+ if (microtime(true) - $this->analysisStartTime < self::RESULT_CACHE_CI_NOTIFICATION_ELAPSED_LIMIT) {
+ return;
+ }
+
+ if (!(new CiDetector())->isCiDetected()) {
+ return;
+ }
+
+ $errorOutput->writeLineFormatted('This analysis took more than a minute and the result cache was not present.');
+ $errorOutput->writeLineFormatted('Persisting PHPStan\'s result cache directory between CI runs will speed up subsequent analyses.');
+ $errorOutput->writeLineFormatted('Learn how to set it up: https://phpstan.org/user-guide/result-cache');
+ $errorOutput->writeLineFormatted('');
+ }
+
private function createStreamOutput(): StreamOutput
{
$resource = fopen('php://memory', 'w', false);
diff --git a/src/Command/AnalysisResult.php b/src/Command/AnalysisResult.php
index bdcb5fd1961..3a100dbc4de 100644
--- a/src/Command/AnalysisResult.php
+++ b/src/Command/AnalysisResult.php
@@ -39,6 +39,7 @@ public function __construct(
private bool $isResultCacheUsed,
private array $changedProjectExtensionFilesOutsideOfAnalysedPaths,
private array $processedFiles = [],
+ private bool $resultCacheExisted = true,
)
{
usort(
@@ -142,6 +143,16 @@ public function isResultCacheUsed(): bool
return $this->isResultCacheUsed;
}
+ /**
+ * Whether the result cache file existed when PHPStan started.
+ * False means the cache was never created; a stale/invalid cache that
+ * triggered a full re-analysis still counts as existing.
+ */
+ public function resultCacheExisted(): bool
+ {
+ return $this->resultCacheExisted;
+ }
+
/**
* @return array
*/
@@ -177,6 +188,7 @@ public function withFileSpecificErrors(array $fileSpecificErrors): self
$this->isResultCacheUsed,
$this->changedProjectExtensionFilesOutsideOfAnalysedPaths,
$this->processedFiles,
+ $this->resultCacheExisted,
);
}
diff --git a/tests/PHPStan/Command/ResultCacheCiNotificationTest.php b/tests/PHPStan/Command/ResultCacheCiNotificationTest.php
new file mode 100644
index 00000000000..8ed073d3bc6
--- /dev/null
+++ b/tests/PHPStan/Command/ResultCacheCiNotificationTest.php
@@ -0,0 +1,140 @@
+originalGithubActions = getenv('GITHUB_ACTIONS');
+ $this->workingDir = sys_get_temp_dir() . '/phpstan-result-cache-ci-' . uniqid();
+ mkdir($this->workingDir);
+ mkdir($this->workingDir . '/src');
+ mkdir($this->workingDir . '/tmp');
+ FileWriter::write($this->workingDir . '/src/Foo.php', "workingDir . '/phpstan.neon', sprintf("parameters:\n\tlevel: 0\n\ttmpDir: %s\n", $this->workingDir . '/tmp'));
+ }
+
+ #[Override]
+ protected function tearDown(): void
+ {
+ parent::tearDown();
+
+ if ($this->originalGithubActions === false) {
+ putenv('GITHUB_ACTIONS');
+ } else {
+ putenv('GITHUB_ACTIONS=' . $this->originalGithubActions);
+ }
+
+ self::deleteDirectory($this->workingDir);
+ }
+
+ public function testNotifiesInCiOnSlowFullAnalysisWithoutResultCache(): void
+ {
+ putenv('GITHUB_ACTIONS=true');
+
+ $output = $this->runCommand(microtime(true) - 120);
+ $this->assertStringContainsString(self::MESSAGE, $output);
+ }
+
+ public function testDoesNotNotifyWhenResultCacheExists(): void
+ {
+ putenv('GITHUB_ACTIONS=true');
+
+ // first run creates the result cache
+ $this->runCommand(microtime(true) - 120);
+
+ // second run finds the cache present
+ $output = $this->runCommand(microtime(true) - 120);
+ $this->assertStringNotContainsString(self::MESSAGE, $output);
+ }
+
+ public function testDoesNotNotifyWhenAnalysisIsFast(): void
+ {
+ putenv('GITHUB_ACTIONS=true');
+
+ $output = $this->runCommand(microtime(true));
+ $this->assertStringNotContainsString(self::MESSAGE, $output);
+ }
+
+ public function testDoesNotNotifyOutsideCi(): void
+ {
+ putenv('GITHUB_ACTIONS');
+
+ $output = $this->runCommand(microtime(true) - 120);
+ $this->assertStringNotContainsString(self::MESSAGE, $output);
+ }
+
+ private function runCommand(float $analysisStartTime): string
+ {
+ $commandTester = new CommandTester(new AnalyseCommand([], $analysisStartTime));
+
+ $commandTester->execute([
+ 'paths' => [$this->workingDir . DIRECTORY_SEPARATOR . 'src'],
+ '--configuration' => $this->workingDir . DIRECTORY_SEPARATOR . 'phpstan.neon',
+ '--debug' => true,
+ ], ['debug' => true]);
+
+ return $commandTester->getDisplay();
+ }
+
+ private static function deleteDirectory(string $directory): void
+ {
+ if (!is_dir($directory)) {
+ return;
+ }
+
+ $entries = scandir($directory);
+ if ($entries === false) {
+ throw new ShouldNotHappenException();
+ }
+
+ foreach ($entries as $entry) {
+ if ($entry === '.' || $entry === '..') {
+ continue;
+ }
+
+ $path = $directory . DIRECTORY_SEPARATOR . $entry;
+ if (is_dir($path)) {
+ self::deleteDirectory($path);
+ } else {
+ unlink($path);
+ }
+ }
+
+ rmdir($directory);
+ }
+
+}
From b99f0fc32263c7dc55d5f220fe395f06a0be02e3 Mon Sep 17 00:00:00 2001
From: phpstan-bot
Date: Tue, 30 Jun 2026 05:37:02 +0000
Subject: [PATCH 2/4] Gate result cache CI notification behind a bleeding edge
feature toggle
Add the notifyAboutMissingResultCacheInCi feature toggle (off by default,
on under bleedingEdge.neon) and only emit the missing-result-cache CI hint
when it is enabled.
Co-Authored-By: Claude Opus 4.8
---
conf/bleedingEdge.neon | 1 +
conf/config.neon | 1 +
conf/parametersSchema.neon | 1 +
src/Command/AnalyseCommand.php | 13 +++++++++++--
.../Command/ResultCacheCiNotificationTest.php | 2 +-
5 files changed, 15 insertions(+), 3 deletions(-)
diff --git a/conf/bleedingEdge.neon b/conf/bleedingEdge.neon
index 2ad962c8d32..55b3ab6cdcf 100644
--- a/conf/bleedingEdge.neon
+++ b/conf/bleedingEdge.neon
@@ -22,3 +22,4 @@ parameters:
checkDynamicConstantNameValues: true
unusedLabel: true
newOnNonObject: true
+ notifyAboutMissingResultCacheInCi: true
diff --git a/conf/config.neon b/conf/config.neon
index 0af76250814..23e0b509385 100644
--- a/conf/config.neon
+++ b/conf/config.neon
@@ -49,6 +49,7 @@ parameters:
checkDynamicConstantNameValues: false
unusedLabel: false
newOnNonObject: false
+ notifyAboutMissingResultCacheInCi: false
fileExtensions:
- php
checkAdvancedIsset: false
diff --git a/conf/parametersSchema.neon b/conf/parametersSchema.neon
index 7d1ed9047b5..13d08b5586f 100644
--- a/conf/parametersSchema.neon
+++ b/conf/parametersSchema.neon
@@ -51,6 +51,7 @@ parametersSchema:
checkDynamicConstantNameValues: bool()
unusedLabel: bool()
newOnNonObject: bool()
+ notifyAboutMissingResultCacheInCi: bool()
])
fileExtensions: listOf(string())
checkAdvancedIsset: bool()
diff --git a/src/Command/AnalyseCommand.php b/src/Command/AnalyseCommand.php
index 4a9a901259e..8f98cfd7c12 100644
--- a/src/Command/AnalyseCommand.php
+++ b/src/Command/AnalyseCommand.php
@@ -654,7 +654,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int
}
}
- $this->reportMissingResultCacheInCi($errorOutput, $analysisResult, $onlyFiles);
+ $this->reportMissingResultCacheInCi(
+ $errorOutput,
+ $analysisResult,
+ $onlyFiles,
+ $container->getParameter('featureToggles')['notifyAboutMissingResultCacheInCi'],
+ );
$this->runDiagnoseExtensions($container, $inceptionResult->getErrorOutput(), $analysisResult->getProcessedFiles());
@@ -665,8 +670,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int
);
}
- private function reportMissingResultCacheInCi(Output $errorOutput, AnalysisResult $analysisResult, bool $onlyFiles): void
+ private function reportMissingResultCacheInCi(Output $errorOutput, AnalysisResult $analysisResult, bool $onlyFiles, bool $notifyAboutMissingResultCacheInCi): void
{
+ if (!$notifyAboutMissingResultCacheInCi) {
+ return;
+ }
+
if (
$onlyFiles
|| $analysisResult->isResultCacheUsed()
diff --git a/tests/PHPStan/Command/ResultCacheCiNotificationTest.php b/tests/PHPStan/Command/ResultCacheCiNotificationTest.php
index 8ed073d3bc6..8a6a9eabbfb 100644
--- a/tests/PHPStan/Command/ResultCacheCiNotificationTest.php
+++ b/tests/PHPStan/Command/ResultCacheCiNotificationTest.php
@@ -44,7 +44,7 @@ protected function setUp(): void
mkdir($this->workingDir . '/src');
mkdir($this->workingDir . '/tmp');
FileWriter::write($this->workingDir . '/src/Foo.php', "workingDir . '/phpstan.neon', sprintf("parameters:\n\tlevel: 0\n\ttmpDir: %s\n", $this->workingDir . '/tmp'));
+ FileWriter::write($this->workingDir . '/phpstan.neon', sprintf("includes:\n\t- %s\n\nparameters:\n\tlevel: 0\n\ttmpDir: %s\n", __DIR__ . '/../../../conf/bleedingEdge.neon', $this->workingDir . '/tmp'));
}
#[Override]
From 6bc8bddc2309ecdf68e4ef16b4972b4c274c4b04 Mon Sep 17 00:00:00 2001
From: phpstan-bot
Date: Tue, 30 Jun 2026 07:56:34 +0000
Subject: [PATCH 3/4] Enable result cache CI notification for everyone and make
the threshold configurable
Drops the bleeding-edge-only feature toggle so the slow-full-analysis hint
runs for all users, and replaces the hardcoded 60s constant with a
resultCacheCiNotificationSeconds parameter. The message is reworded to be a
clearer call to action about speeding up CI.
Co-Authored-By: Claude Opus 4.8
---
conf/bleedingEdge.neon | 1 -
conf/config.neon | 2 +-
conf/parametersSchema.neon | 2 +-
src/Command/AnalyseCommand.php | 17 +--
.../Command/ResultCacheCiNotificationTest.php | 140 ------------------
5 files changed, 8 insertions(+), 154 deletions(-)
delete mode 100644 tests/PHPStan/Command/ResultCacheCiNotificationTest.php
diff --git a/conf/bleedingEdge.neon b/conf/bleedingEdge.neon
index 55b3ab6cdcf..2ad962c8d32 100644
--- a/conf/bleedingEdge.neon
+++ b/conf/bleedingEdge.neon
@@ -22,4 +22,3 @@ parameters:
checkDynamicConstantNameValues: true
unusedLabel: true
newOnNonObject: true
- notifyAboutMissingResultCacheInCi: true
diff --git a/conf/config.neon b/conf/config.neon
index 23e0b509385..f28d2b30259 100644
--- a/conf/config.neon
+++ b/conf/config.neon
@@ -49,7 +49,6 @@ parameters:
checkDynamicConstantNameValues: false
unusedLabel: false
newOnNonObject: false
- notifyAboutMissingResultCacheInCi: false
fileExtensions:
- php
checkAdvancedIsset: false
@@ -168,6 +167,7 @@ parameters:
resultCachePath: %tmpDir%/resultCache.php
resultCacheSkipIfOlderThanDays: 7
resultCacheChecksProjectExtensionFilesDependencies: false
+ resultCacheCiNotificationSeconds: 60
dynamicConstantNames:
- ICONV_IMPL
- LIBXML_VERSION
diff --git a/conf/parametersSchema.neon b/conf/parametersSchema.neon
index 13d08b5586f..3d7a21a858f 100644
--- a/conf/parametersSchema.neon
+++ b/conf/parametersSchema.neon
@@ -51,7 +51,6 @@ parametersSchema:
checkDynamicConstantNameValues: bool()
unusedLabel: bool()
newOnNonObject: bool()
- notifyAboutMissingResultCacheInCi: bool()
])
fileExtensions: listOf(string())
checkAdvancedIsset: bool()
@@ -162,6 +161,7 @@ parametersSchema:
resultCachePath: string()
resultCacheSkipIfOlderThanDays: int()
resultCacheChecksProjectExtensionFilesDependencies: bool()
+ resultCacheCiNotificationSeconds: int()
dynamicConstantNames: arrayOf(string())
customRulesetUsed: schema(bool(), nullable())
rootDir: string()
diff --git a/src/Command/AnalyseCommand.php b/src/Command/AnalyseCommand.php
index 8f98cfd7c12..bbb186465af 100644
--- a/src/Command/AnalyseCommand.php
+++ b/src/Command/AnalyseCommand.php
@@ -77,8 +77,6 @@ final class AnalyseCommand extends Command
public const DEFAULT_LEVEL = CommandHelper::DEFAULT_LEVEL;
- private const RESULT_CACHE_CI_NOTIFICATION_ELAPSED_LIMIT = 60.0;
-
/**
* @param string[] $composerAutoloaderProjectPaths
*/
@@ -658,7 +656,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$errorOutput,
$analysisResult,
$onlyFiles,
- $container->getParameter('featureToggles')['notifyAboutMissingResultCacheInCi'],
+ $container->getParameter('resultCacheCiNotificationSeconds'),
);
$this->runDiagnoseExtensions($container, $inceptionResult->getErrorOutput(), $analysisResult->getProcessedFiles());
@@ -670,12 +668,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int
);
}
- private function reportMissingResultCacheInCi(Output $errorOutput, AnalysisResult $analysisResult, bool $onlyFiles, bool $notifyAboutMissingResultCacheInCi): void
+ private function reportMissingResultCacheInCi(Output $errorOutput, AnalysisResult $analysisResult, bool $onlyFiles, int $resultCacheCiNotificationSeconds): void
{
- if (!$notifyAboutMissingResultCacheInCi) {
- return;
- }
-
if (
$onlyFiles
|| $analysisResult->isResultCacheUsed()
@@ -684,7 +678,7 @@ private function reportMissingResultCacheInCi(Output $errorOutput, AnalysisResul
return;
}
- if (microtime(true) - $this->analysisStartTime < self::RESULT_CACHE_CI_NOTIFICATION_ELAPSED_LIMIT) {
+ if (microtime(true) - $this->analysisStartTime < $resultCacheCiNotificationSeconds) {
return;
}
@@ -692,8 +686,9 @@ private function reportMissingResultCacheInCi(Output $errorOutput, AnalysisResul
return;
}
- $errorOutput->writeLineFormatted('This analysis took more than a minute and the result cache was not present.');
- $errorOutput->writeLineFormatted('Persisting PHPStan\'s result cache directory between CI runs will speed up subsequent analyses.');
+ $errorOutput->writeLineFormatted('Tip: This CI run analysed your whole project from scratch, which is why it was slow.');
+ $errorOutput->writeLineFormatted('Persist PHPStan\'s result cache directory between CI runs to make your pipeline dramatically faster.');
+ $errorOutput->writeLineFormatted('Only changed files and their dependencies are re-analysed, so most runs finish in a fraction of the time.');
$errorOutput->writeLineFormatted('Learn how to set it up: https://phpstan.org/user-guide/result-cache');
$errorOutput->writeLineFormatted('');
}
diff --git a/tests/PHPStan/Command/ResultCacheCiNotificationTest.php b/tests/PHPStan/Command/ResultCacheCiNotificationTest.php
deleted file mode 100644
index 8a6a9eabbfb..00000000000
--- a/tests/PHPStan/Command/ResultCacheCiNotificationTest.php
+++ /dev/null
@@ -1,140 +0,0 @@
-originalGithubActions = getenv('GITHUB_ACTIONS');
- $this->workingDir = sys_get_temp_dir() . '/phpstan-result-cache-ci-' . uniqid();
- mkdir($this->workingDir);
- mkdir($this->workingDir . '/src');
- mkdir($this->workingDir . '/tmp');
- FileWriter::write($this->workingDir . '/src/Foo.php', "workingDir . '/phpstan.neon', sprintf("includes:\n\t- %s\n\nparameters:\n\tlevel: 0\n\ttmpDir: %s\n", __DIR__ . '/../../../conf/bleedingEdge.neon', $this->workingDir . '/tmp'));
- }
-
- #[Override]
- protected function tearDown(): void
- {
- parent::tearDown();
-
- if ($this->originalGithubActions === false) {
- putenv('GITHUB_ACTIONS');
- } else {
- putenv('GITHUB_ACTIONS=' . $this->originalGithubActions);
- }
-
- self::deleteDirectory($this->workingDir);
- }
-
- public function testNotifiesInCiOnSlowFullAnalysisWithoutResultCache(): void
- {
- putenv('GITHUB_ACTIONS=true');
-
- $output = $this->runCommand(microtime(true) - 120);
- $this->assertStringContainsString(self::MESSAGE, $output);
- }
-
- public function testDoesNotNotifyWhenResultCacheExists(): void
- {
- putenv('GITHUB_ACTIONS=true');
-
- // first run creates the result cache
- $this->runCommand(microtime(true) - 120);
-
- // second run finds the cache present
- $output = $this->runCommand(microtime(true) - 120);
- $this->assertStringNotContainsString(self::MESSAGE, $output);
- }
-
- public function testDoesNotNotifyWhenAnalysisIsFast(): void
- {
- putenv('GITHUB_ACTIONS=true');
-
- $output = $this->runCommand(microtime(true));
- $this->assertStringNotContainsString(self::MESSAGE, $output);
- }
-
- public function testDoesNotNotifyOutsideCi(): void
- {
- putenv('GITHUB_ACTIONS');
-
- $output = $this->runCommand(microtime(true) - 120);
- $this->assertStringNotContainsString(self::MESSAGE, $output);
- }
-
- private function runCommand(float $analysisStartTime): string
- {
- $commandTester = new CommandTester(new AnalyseCommand([], $analysisStartTime));
-
- $commandTester->execute([
- 'paths' => [$this->workingDir . DIRECTORY_SEPARATOR . 'src'],
- '--configuration' => $this->workingDir . DIRECTORY_SEPARATOR . 'phpstan.neon',
- '--debug' => true,
- ], ['debug' => true]);
-
- return $commandTester->getDisplay();
- }
-
- private static function deleteDirectory(string $directory): void
- {
- if (!is_dir($directory)) {
- return;
- }
-
- $entries = scandir($directory);
- if ($entries === false) {
- throw new ShouldNotHappenException();
- }
-
- foreach ($entries as $entry) {
- if ($entry === '.' || $entry === '..') {
- continue;
- }
-
- $path = $directory . DIRECTORY_SEPARATOR . $entry;
- if (is_dir($path)) {
- self::deleteDirectory($path);
- } else {
- unlink($path);
- }
- }
-
- rmdir($directory);
- }
-
-}
From fdb299214762e3ce4802bd4e0c009c7bc9bd341a Mon Sep 17 00:00:00 2001
From: phpstan-bot
Date: Tue, 30 Jun 2026 07:56:50 +0000
Subject: [PATCH 4/4] Cover result cache CI notification with an E2E test
Adds an e2e/result-cache-ci-notification case driven from e2e-tests.yml:
a fresh run emits the hint, the cached re-run does not. Replaces the
removed CommandTester-based unit test.
Co-Authored-By: Claude Opus 4.8
---
.github/workflows/e2e-tests.yml | 14 ++++++++++++++
e2e/result-cache-ci-notification/phpstan.neon | 6 ++++++
e2e/result-cache-ci-notification/src/Foo.php | 13 +++++++++++++
3 files changed, 33 insertions(+)
create mode 100644 e2e/result-cache-ci-notification/phpstan.neon
create mode 100644 e2e/result-cache-ci-notification/src/Foo.php
diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml
index 208bb0e5069..36c45eb4705 100644
--- a/.github/workflows/e2e-tests.yml
+++ b/.github/workflows/e2e-tests.yml
@@ -352,6 +352,20 @@ jobs:
composer install
../../bin/phpstan -vvv
../../bin/phpstan -vvv
+ - script: |
+ cd e2e/result-cache-ci-notification
+ # https://github.com/phpstan/phpstan/issues/14881
+ # A slow full analysis in CI without a pre-existing result cache should hint
+ # the user to persist the result cache directory between runs.
+ ../../bin/phpstan clear-result-cache
+ OUTPUT=$(../../bin/phpstan analyse 2>&1)
+ echo "$OUTPUT"
+ ../bashunit -a contains 'Persist PHPStan' "$OUTPUT"
+ ../bashunit -a contains 'to make your pipeline dramatically faster' "$OUTPUT"
+ # The second run finds the cache present, so the hint is not shown again.
+ OUTPUT=$(../../bin/phpstan analyse 2>&1)
+ echo "$OUTPUT"
+ ../bashunit -a not_contains 'to make your pipeline dramatically faster' "$OUTPUT"
- script: |
cd e2e/bug-12606
export CONFIGTEST=test
diff --git a/e2e/result-cache-ci-notification/phpstan.neon b/e2e/result-cache-ci-notification/phpstan.neon
new file mode 100644
index 00000000000..101d854bece
--- /dev/null
+++ b/e2e/result-cache-ci-notification/phpstan.neon
@@ -0,0 +1,6 @@
+parameters:
+ level: 0
+ paths:
+ - src
+ # Notify already after 0 seconds so the slow-analysis condition is always met in the test.
+ resultCacheCiNotificationSeconds: 0
diff --git a/e2e/result-cache-ci-notification/src/Foo.php b/e2e/result-cache-ci-notification/src/Foo.php
new file mode 100644
index 00000000000..fa70dd29416
--- /dev/null
+++ b/e2e/result-cache-ci-notification/src/Foo.php
@@ -0,0 +1,13 @@
+