Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions .github/workflows/quality.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
name: EvolvePHP 2 Quality

on:
pull_request:
branches:
- 2.x
push:
branches:
- 2.x
workflow_dispatch:

permissions:
contents: read

concurrency:
group: evolvephp-2-quality-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
policy:
name: Policy (PHP 8.4)
runs-on: ubuntu-24.04

steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Set up PHP
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2
with:
php-version: '8.4'
tools: composer:v2
coverage: none

- name: Validate workspace Composer manifest and lockfile
run: composer --working-dir=workspace validate --strict --check-lock

- name: Install workspace dependencies
run: composer --working-dir=workspace install --no-interaction --no-progress --prefer-dist

- name: Run root policy tests
run: php workspace/vendor/bin/phpunit --configuration phpunit.xml.dist tests/Architecture tests/Documentation

workspace-quality:
name: Workspace quality (PHP ${{ matrix.php }})
runs-on: ubuntu-24.04

strategy:
fail-fast: false
matrix:
php:
- '8.4'
- '8.5'

steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Set up PHP
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2
with:
php-version: ${{ matrix.php }}
tools: composer:v2
coverage: none

- name: Validate workspace Composer manifest and lockfile
run: composer --working-dir=workspace validate --strict --check-lock

- name: Install workspace dependencies
run: composer --working-dir=workspace install --no-interaction --no-progress --prefer-dist

- name: Run workspace quality
run: composer --working-dir=workspace quality
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### Documentation and governance

- Added Phase 2.6 GitHub Actions CI foundation with a PHP 8.4/8.5 workspace quality matrix, separate PHP 8.4 root-policy job, lockfile-based workspace installation, immutable action pinning, and successful initial CI execution for the current workspace, tooling and package foundation.
- Added Phase 2.5.1 README and metadata consistency cleanup, aligning the EvolvePHP 2 README hierarchy, correcting root Composer metadata, deduplicating stale RFC index narration, removing duplicated phase history, and clarifying workspace installation and compatibility guidance.
- Added Phase 2.5 workspace-owned Deptrac architecture and dependency-boundary enforcement for the initial EvolvePHP 2 package graph.
- Added Phase 2.4 workspace-owned PHPStan level 6 static analysis, PHPUnit type-inference integration, PHP-CS-Fixer PER Coding Style 3.0 checks, and architecture-policy tests for cache, baseline and tooling ownership.
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ EvolvePHP 1 remains preserved on `master` for historical reference and legacy ma

The current EvolvePHP 2 repository contains package boundaries, Composer workspace setup and quality-tooling foundations. Runtime framework implementation is not yet complete, and the packages are not yet published.

EvolvePHP 2 requires PHP 8.4. Local PHP 8.4 validation has been performed. PHP 8.5 remains pending Phase 2.6 CI evidence.
EvolvePHP 2 requires PHP 8.4. The current workspace quality pipeline is verified by GitHub Actions on PHP 8.4 and PHP 8.5 for the current workspace, quality tooling and package foundation.

## Project Overview

Expand All @@ -21,7 +21,7 @@ The EvolvePHP 2 line redesigns the framework as a modular package architecture w
- Runtime implementation: not yet complete
- Package publication: packages are not yet published
- PHP baseline: PHP 8.4
- PHP 8.5: compatibility remains pending Phase 2.6 CI evidence
- CI verification: current workspace quality passes in GitHub Actions on PHP 8.4 and PHP 8.5

## Requirements

Expand Down
206 changes: 206 additions & 0 deletions tests/Architecture/EvolvePhp2ContinuousIntegrationTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
<?php

use PHPUnit\Framework\TestCase;

final class EvolvePhp2ContinuousIntegrationTest extends TestCase
{
private $root;
private $workflowPath;
private $workflow;

protected function setUp(): void
{
$this->root = dirname(__DIR__, 2);
$this->workflowPath = $this->projectPath('.github/workflows/quality.yml');

$this->assertFileExists($this->workflowPath, 'The canonical EvolvePHP 2 quality workflow should exist.');

$this->workflow = file_get_contents($this->workflowPath);
$this->assertNotFalse($this->workflow, 'The canonical EvolvePHP 2 quality workflow should be readable.');
}

public function testCanonicalWorkflowNameAndTriggersTargetOnlyEvolvePhp2(): void
{
$this->assertMatchesPattern('/^name:\s*EvolvePHP 2 Quality\s*$/m', $this->workflow);
$this->assertMatchesPattern('/^on:\s*$/m', $this->workflow);
$this->assertMatchesPattern('/^\s{2}pull_request:\s*\R\s{4}branches:\s*\R\s{6}- 2\.x\s*$/m', $this->workflow);
$this->assertMatchesPattern('/^\s{2}push:\s*\R\s{4}branches:\s*\R\s{6}- 2\.x\s*$/m', $this->workflow);
$this->assertMatchesPattern('/^\s{2}workflow_dispatch:\s*$/m', $this->workflow);
$this->assertDoesNotMatchPattern('/^\s*-\s*master\s*$/m', $this->workflow);
$this->assertDoesNotMatchPattern('/pull_request_target/', $this->workflow);
$this->assertDoesNotMatchPattern('/paths(?:-ignore)?:/', $this->workflow);
$this->assertDoesNotMatchPattern('/tags(?:-ignore)?:/', $this->workflow);
}

public function testWorkflowUsesLeastPrivilegePermissionsAndCancelsSupersededRuns(): void
{
$this->assertMatchesPattern('/^permissions:\s*\R\s{2}contents:\s*read\s*$/m', $this->workflow);
$this->assertSame(1, preg_match_all('/^\s{2}[a-z-]+:\s*(?:read|write|none)\s*$/m', $this->extractTopLevelBlock('permissions'), $matches));
$this->assertDoesNotMatchPattern('/\bwrite\b/', $this->extractTopLevelBlock('permissions'));
$this->assertMatchesPattern('/^concurrency:\s*$/m', $this->workflow);
$this->assertMatchesPattern('/^\s{2}group:\s*evolvephp-2-quality-\$\{\{ github\.workflow \}\}-\$\{\{ github\.event\.pull_request\.number \|\| github\.ref \}\}\s*$/m', $this->workflow);
$this->assertMatchesPattern('/^\s{2}cancel-in-progress:\s*true\s*$/m', $this->workflow);
}

public function testWorkflowUsesOnlyApprovedUbuntuRunnerAndPhpMatrix(): void
{
$this->assertSame(2, preg_match_all('/^\s{4}runs-on:\s*ubuntu-24\.04\s*$/m', $this->workflow, $matches));
$this->assertDoesNotMatchPattern('/ubuntu-latest|windows-|macos-/', $this->workflow);
$this->assertMatchesPattern('/fail-fast:\s*false/', $this->workflow);
$this->assertMatchesPattern('/php:\s*\R\s{10}- \'8\.4\'\s*\R\s{10}- \'8\.5\'/m', $this->workflow);
$this->assertSame(2, preg_match_all('/^\s{10}- \'8\.[45]\'\s*$/m', $this->workflow, $matches));
$this->assertDoesNotMatchPattern('/nightly|experimental|lowest|highest|latest|8\.6/', $this->workflow);
}

public function testWorkflowPinsOnlyReviewedActionReleaseCommits(): void
{
$checkoutSha = '3d3c42e5aac5ba805825da76410c181273ba90b1';
$setupPhpSha = 'f3e473d116dcccaddc5834248c87452386958240';

$this->assertSame(2, preg_match_all('/uses:\s*actions\/checkout@' . $checkoutSha . '\s+# v7\.0\.1/', $this->workflow, $matches));
$this->assertSame(2, preg_match_all('/uses:\s*shivammathur\/setup-php@' . $setupPhpSha . '\s+# 2\.37\.2/', $this->workflow, $matches));
$this->assertSame(4, preg_match_all('/uses:\s*[^@\s]+@[0-9a-f]{40}\s+# (?:v7\.0\.1|2\.37\.2)/', $this->workflow, $matches));
$this->assertMatchesPattern('/persist-credentials:\s*false/', $this->workflow);
$this->assertSame(2, preg_match_all('/tools:\s*composer:v2/', $this->workflow, $matches));
$this->assertSame(2, preg_match_all('/coverage:\s*none/', $this->workflow, $matches));
$this->assertDoesNotMatchPattern('/actions\/checkout@(?:v[0-9]+|main)|shivammathur\/setup-php@(?:v[0-9]+|main)/', $this->workflow);
$this->assertDoesNotMatchPattern('/uses:\s*(?!actions\/checkout@|shivammathur\/setup-php@)[^@\s]+@/', $this->workflow);
$this->assertDoesNotMatchPattern('/@[0-9a-f]{7,39}(?:\s|$)/', $this->workflow);
$this->assertDoesNotMatchPattern('/actions\/cache/', $this->workflow);
}

public function testPolicyJobRunsRootPolicySuitesWithWorkspacePhpUnitOnPhp84(): void
{
$job = $this->extractJob('policy');

$this->assertMatchesPattern('/name:\s*Policy \(PHP 8\.4\)/', $job);
$this->assertMatchesPattern('/runs-on:\s*ubuntu-24\.04/', $job);
$this->assertMatchesPattern('/php-version:\s*\'8\.4\'/', $job);
$this->assertStringContainsString('composer --working-dir=workspace validate --strict --check-lock', $job);
$this->assertStringContainsString('composer --working-dir=workspace install --no-interaction --no-progress --prefer-dist', $job);
$this->assertStringContainsString('php workspace/vendor/bin/phpunit --configuration phpunit.xml.dist tests/Architecture tests/Documentation', $job);
$this->assertDoesNotMatchPattern('/composer install(?! --working-dir=workspace)|composer --working-dir=\.\s+install/', $job);
$this->assertDoesNotMatchPattern('/composer update|--ignore-platform-reqs?|config\.platform\.php/', $job);
$this->assertDoesNotMatchPattern('/phpunit.*(?:core|components|helpers|index\.php|route\.php)/i', $job);
}

public function testWorkspaceQualityMatrixUsesLockfileInstallAndApprovedAggregateCommand(): void
{
$job = $this->extractJob('workspace-quality');

$this->assertMatchesPattern('/name:\s*Workspace quality \(PHP \$\{\{ matrix\.php \}\}\)/', $job);
$this->assertMatchesPattern('/strategy:\s*\R\s{6}fail-fast:\s*false/', $job);
$this->assertMatchesPattern('/php:\s*\R\s{10}- \'8\.4\'\s*\R\s{10}- \'8\.5\'/m', $job);
$this->assertStringContainsString('composer --working-dir=workspace validate --strict --check-lock', $job);
$this->assertStringContainsString('composer --working-dir=workspace install --no-interaction --no-progress --prefer-dist', $job);
$this->assertStringContainsString('composer --working-dir=workspace quality', $job);
$this->assertDoesNotMatchPattern('/composer update|style:fix|continue-on-error|--ignore-platform-reqs?/', $job);
$this->assertDoesNotMatchPattern('/composer --working-dir=workspace (architecture|analyse|style:check|test)(?:\s|$)/', $job);
}

public function testWorkflowExcludesReleasePublishingSecretsCachesAndDeployments(): void
{
foreach (array(
'/secrets\./',
'/deploy(?:ment)?/i',
'/publish/i',
'/release/i',
'/upload-artifact/',
'/codecov|coveralls/i',
'/actions\/cache/',
'/environment:/',
'/sudo\b/',
'/composer install(?! --working-dir=workspace)/',
'/--ignore-platform-reqs?/',
'/config\.platform\.php/',
) as $pattern) {
$this->assertDoesNotMatchPattern($pattern, $this->workflow);
}
}

public function testDocumentationRecordsContinuousIntegrationCompatibilityEvidence(): void
{
$workspaceReadme = $this->readProjectFile('workspace/README.md');
$changelog = $this->readProjectFile('CHANGELOG.md');

foreach (array(
'/## Continuous Integration/',
'/\.github\/workflows\/quality\.yml/',
'/EvolvePHP 2 Quality/',
'/pull requests.*2\.x|2\.x.*pull requests/i',
'/pushes.*2\.x|2\.x.*pushes/i',
'/manual.*dispatch|workflow_dispatch/i',
'/contents:\s*read/',
'/concurrency.*cancel|cancel.*concurrency/i',
'/Ubuntu 24\.04/',
'/policy job.*PHP 8\.4|PHP 8\.4.*policy job/i',
'/Architecture and Documentation.*workspace PHPUnit 13|workspace PHPUnit 13.*Architecture and Documentation/i',
'/EvolvePHP 1 runtime.*not part|not part.*EvolvePHP 1 runtime/i',
'/workspace quality matrix.*PHP 8\.4.*PHP 8\.5|PHP 8\.4.*PHP 8\.5.*workspace quality matrix/i',
'/Composer validation.*before.*install|validate.*before.*install/i',
'/lockfile.*composer install|composer install.*lockfile/i',
'/no `composer update`|not run `composer update`/i',
'/no platform-requirement bypass|platform-requirement bypass.*not/i',
'/no initial dependency cache|dependency cache.*not/i',
'/immutable.*full-SHA|full-SHA.*immutable/i',
'/release comments.*SHA|SHA.*release comments/i',
'/Phase 2\.6 CI matrix.*successfully executed|successfully executed.*Phase 2\.6 CI matrix/i',
'/workspace quality.*passes.*PHP 8\.4.*PHP 8\.5|PHP 8\.4.*PHP 8\.5.*workspace quality.*passes/i',
'/current.*(?:workspace|tooling|package foundation)|(?:workspace|tooling|package foundation).*current/i',
'/EvolvePHP 1 runtime.*(?:not part|excluded)|(?:not part|excluded).*EvolvePHP 1 runtime/i',
'/runtime implementation.*(?:incomplete|not complete)|(?:incomplete|not complete).*runtime implementation/i',
) as $pattern) {
$this->assertMatchesPattern($pattern, $workspaceReadme);
}

$this->assertMatchesPattern('/Phase 2\.6/i', $changelog);
$this->assertMatchesPattern('/PHP 8\.4\/8\.5 workspace quality matrix/i', $changelog);
$this->assertMatchesPattern('/successful initial CI execution|initial GitHub Actions execution completed successfully|CI execution.*successfully/i', $changelog);
$this->assertMatchesPattern('/separate PHP 8\.4 root-policy job/i', $changelog);
$this->assertMatchesPattern('/lockfile-based workspace installation/i', $changelog);
$this->assertMatchesPattern('/immutable action pinning/i', $changelog);
$this->assertDoesNotMatchPattern('/branch protection.*active|required checks|deployment|publishing|runtime implementation.*complete|legacy EvolvePHP 1 runtime.*PHP 8\.5/i', $changelog);
}

private function extractTopLevelBlock($heading)
{
$pattern = '/^' . preg_quote($heading, '/') . ':\s*\R(?P<block>.*?)(?=^[a-zA-Z_-]+:\s*|\z)/ms';

$this->assertSame(1, preg_match($pattern, $this->workflow, $matches), 'Missing top-level block: ' . $heading);

return $matches['block'];
}

private function extractJob($job)
{
$jobs = $this->extractTopLevelBlock('jobs');
$pattern = '/^\s{2}' . preg_quote($job, '/') . ':\s*\R(?P<job>.*?)(?=^\s{2}[a-zA-Z0-9_-]+:\s*|\z)/ms';

$this->assertSame(1, preg_match($pattern, $jobs, $matches), 'Missing job: ' . $job);

return $matches['job'];
}

private function projectPath($path)
{
return $this->root . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $path);
}

private function readProjectFile($path)
{
$fullPath = $this->projectPath($path);
$this->assertFileExists($fullPath, $path . ' should exist before it is read.');

return file_get_contents($fullPath);
}

private function assertMatchesPattern($pattern, $content)
{
$this->assertSame(1, preg_match($pattern, $content), 'Failed asserting that content matches ' . $pattern);
}

private function assertDoesNotMatchPattern($pattern, $content)
{
$this->assertSame(0, preg_match($pattern, $content), 'Failed asserting that content does not match ' . $pattern);
}
}
6 changes: 4 additions & 2 deletions tests/Architecture/EvolvePhp2PhpUnitFoundationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,10 @@ public function testWorkspaceReadmeDocumentsPhpUnitFoundationPolicy(): void
$this->assertMatchesPattern('/test:testing/i', $content);
$this->assertMatchesPattern('/workspace\/composer\.lock/i', $content);
$this->assertMatchesPattern('/platform emulation/i', $content);
$this->assertMatchesPattern('/PHP 8\.5 compatibility.*pending.*Phase 2\.6 CI matrix|Phase 2\.6 CI matrix.*pending.*PHP 8\.5 compatibility/i', $content);
$this->assertDoesNotMatchPattern('/PHP 8\.5[^.\n]*(?:passes|supported|compatible)/i', $content);
$this->assertMatchesPattern('/PHP 8\.4.*baseline|baseline.*PHP 8\.4/i', $content);
$this->assertMatchesPattern('/GitHub Actions.*workspace quality.*PHP 8\.4.*PHP 8\.5|workspace quality.*GitHub Actions.*PHP 8\.4.*PHP 8\.5|PHP 8\.4.*PHP 8\.5.*workspace quality.*GitHub Actions/i', $content);
$this->assertMatchesPattern('/current.*(?:workspace quality|package foundation|tooling)|(?:workspace quality|package foundation|tooling).*current/i', $content);
$this->assertDoesNotMatchPattern('/PHP 8\.5.*pending|pending.*PHP 8\.5|Phase 2\.6.*pending|pending.*Phase 2\.6/i', $content);
$this->assertMatchesPattern('/legacy root suite/i', $content);
$this->assertMatchesPattern('/EvolvePHP 2 workspace suite/i', $content);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,18 @@ public function testRootReadmeIdentifiesEvolvePhp2BranchAndLegacyMasterLine(): v
$this->assertMatchesPattern('/packages.*not yet published|not yet published.*packages/is', $content);
}

public function testRootReadmeDocumentsPhp84WithoutClaimingPhp85Compatibility(): void
public function testRootReadmeDocumentsPhp84BaselineAndPhp85CiEvidence(): void
{
$content = $this->readProjectFile('README.md');

$this->assertMatchesPattern('/requires PHP 8\.4|PHP 8\.4.*required/i', $content);
$this->assertMatchesPattern('/PHP 8\.4.*validation|validation.*PHP 8\.4/is', $content);
$this->assertMatchesPattern('/PHP 8\.5.*Phase 2\.6|Phase 2\.6.*PHP 8\.5/i', $content);
$this->assertDoesNotMatchPattern('/PHP 8\.5[^.\n]*(?:compatible|compatibility is claimed|passes|supported)/i', $content);
$this->assertMatchesPattern('/PHP baseline:\s*PHP 8\.4|PHP 8\.4.*baseline/i', $content);
$this->assertMatchesPattern('/GitHub Actions.*(?:PHP 8\.4.*PHP 8\.5|PHP 8\.5.*PHP 8\.4)|(?:PHP 8\.4.*PHP 8\.5|PHP 8\.5.*PHP 8\.4).*GitHub Actions/i', $content);
$this->assertMatchesPattern('/workspace quality pipeline|quality pipeline.*workspace/i', $content);
$this->assertMatchesPattern('/current.*(?:workspace|tooling|package foundation)|(?:workspace|tooling|package foundation).*current/i', $content);
$this->assertMatchesPattern('/runtime framework implementation.*not yet complete|not yet complete.*runtime framework implementation/i', $content);
$this->assertDoesNotMatchPattern('/PHP 8\.5.*pending|pending.*PHP 8\.5|Do not claim PHP 8\.5/i', $content);
$this->assertDoesNotMatchPattern('/production[- ]ready|runtime framework supports PHP 8\.5|all runtime.*PHP 8\.5/i', $content);
}

public function testRootReadmeUsesWorkspaceSetupAndAvoidsLegacySetupAsEvolvePhp2Setup(): void
Expand Down
Loading