diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9e83bf5..d8b6a0d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,40 +12,32 @@ concurrency: cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: - homeboy: - name: Homeboy ${{ matrix.label }} + checks: + name: ${{ matrix.label }} runs-on: ubuntu-latest strategy: fail-fast: false matrix: include: - - command: lint - label: lint (PHPCS + PHPStan) - - command: test - label: tests (PHPUnit + smoke tests) + - command: phpstan + label: PHPStan + - command: smoke + label: PHP smoke tests steps: - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - name: Generate app token - id: app-token - uses: actions/create-github-app-token@v3 - continue-on-error: true + - name: Setup PHP + uses: shivammathur/setup-php@v2 with: - app-id: ${{ secrets.HOMEBOY_APP_ID }} - private-key: ${{ secrets.HOMEBOY_APP_PRIVATE_KEY }} + php-version: '8.1' + coverage: none - - name: Run Homeboy ${{ matrix.command }} - uses: Extra-Chill/homeboy-action@4b8c76830649c6b929fb64fa38e1169b3f133c9b # v2.7.10 - with: - commands: ${{ matrix.command }} - expected-commands: lint,test - app-token: ${{ steps.app-token.outputs.token || '' }} - autofix: 'true' - autofix-commands: 'lint --fix' - - - name: Run PHP smoke tests - if: matrix.command == 'test' - run: composer smoke + - name: Validate Composer metadata + run: composer validate --strict + + - name: Install dependencies + run: composer install --no-interaction --prefer-dist + + - name: Run ${{ matrix.label }} + run: composer ${{ matrix.command }} diff --git a/README.md b/README.md index ad1e7e7..2f80f07 100644 --- a/README.md +++ b/README.md @@ -70,12 +70,12 @@ Agents API requires **WordPress 7.0 or higher**. The substrate itself is provide ## Quality Gates -Agents API uses [Homeboy](https://github.com/Extra-Chill/homeboy) as the single CI entry point for repository checks. +Agents API runs repository checks directly through Composer scripts: -- `Homeboy lint (PHPCS + PHPStan)` runs WordPress coding standards and PHPStan at max level through Homeboy's WordPress extension config. -- `Homeboy tests (PHP smoke tests)` runs the current PHP smoke-test suite. When PHPUnit coverage is added, this lane should become `Homeboy tests (PHPUnit + smoke tests)`. +- `composer phpstan` runs PHPStan at max level with WordPress stubs. +- `composer smoke` runs the current PHP smoke-test suite. -Keeping these checks behind Homeboy gives reviewers one consistent quality surface while still preserving the underlying tools' strengths: PHPCS catches WordPress style issues, PHPStan catches type and contract drift, and the smoke tests prove the runtime wiring still behaves as expected. +These checks keep the package self-contained for Core-candidate review while proving the runtime wiring and static contracts still behave as expected. ## Core-Candidate API Naming diff --git a/tests/no-product-imports-smoke.php b/tests/no-product-imports-smoke.php index cf1afa8..96d5917 100644 --- a/tests/no-product-imports-smoke.php +++ b/tests/no-product-imports-smoke.php @@ -1,6 +1,6 @@ $agents_api_dir . '/agents-api.php', + 'src' => $agents_api_dir . '/src', +); + $forbidden_namespaces = array( 'ExampleProduct\\Core\\Steps', 'ExampleProduct\\Core\\Database\\Jobs', @@ -31,6 +36,29 @@ 'ExampleProduct\\Core\\Content', ); +$forbidden_product_vocabulary = array( + 'Data Machine', + 'DataMachine', + 'data machine', + 'datamachine', + 'data-machine', + 'data_machine', + 'Homeboy', + 'homeboy', + 'WP Codebox', + 'WP_Codebox', + 'wp-codebox', + 'wp_codebox', + 'Codebox', + 'codebox', + 'wp-site-generator', + 'wp_site_generator', + 'wp site generator', + 'wp-site generator', + 'WPSG', + 'wpsg', +); + $forbidden_admin_apis = array( 'add_menu_page', 'add_submenu_page', @@ -51,45 +79,75 @@ 'admin_post_', ); -$matches = array(); -$iterator = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( (string) $agents_api_dir ) ); -foreach ( $iterator as $file ) { - if ( ! $file->isFile() || 'php' !== $file->getExtension() ) { +$forbidden_table_ownership_patterns = array( + '/\bdbDelta\s*\(/i' => 'runs dbDelta', + '/\bCREATE\s+(?:TEMPORARY\s+)?TABLE\b/i' => 'creates database tables', + '/\bregister_activation_hook\s*\(/i' => 'registers activation hook', +); + +$matches = array(); +$files = array(); +foreach ( $production_paths as $path ) { + if ( is_file( $path ) ) { + $files[] = new SplFileInfo( $path ); continue; } - $relative_path = str_replace( (string) $agents_api_dir . '/', '', $file->getPathname() ); - if ( str_starts_with( $relative_path, 'tests/' ) || str_starts_with( $relative_path, 'vendor/' ) ) { + if ( ! is_dir( $path ) ) { continue; } - $source = (string) file_get_contents( $file->getPathname() ); + $iterator = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $path, FilesystemIterator::SKIP_DOTS ) ); + foreach ( $iterator as $file ) { + if ( $file->isFile() && 'php' === $file->getExtension() ) { + $files[] = $file; + } + } +} + +foreach ( $files as $file ) { + $relative_path = str_replace( (string) $agents_api_dir . '/', '', $file->getPathname() ); + $source = (string) file_get_contents( $file->getPathname() ); + foreach ( $forbidden_namespaces as $namespace ) { $quoted = preg_quote( $namespace, '/' ); if ( preg_match( '/(?:use\s+|new\s+|extends\s+|implements\s+|instanceof\s+|\\\\)' . $quoted . '(?:\\\\|;|\s|\(|::)/', $source ) ) { - $matches[] = str_replace( (string) $agents_api_dir . '/', '', $file->getPathname() ) . ' imports ' . $namespace; + $matches[] = $relative_path . ' imports ' . $namespace; } } if ( preg_match( '/(?:use\s+|new\s+|extends\s+|implements\s+|instanceof\s+)\\?ExampleProduct\\\\/', $source ) ) { - $matches[] = str_replace( (string) $agents_api_dir . '/', '', $file->getPathname() ) . ' imports an ExampleProduct namespace'; + $matches[] = $relative_path . ' imports an ExampleProduct namespace'; } foreach ( $forbidden_admin_apis as $function_name ) { if ( preg_match( '/\\b' . preg_quote( $function_name, '/' ) . '\\s*\(/', $source ) ) { - $matches[] = str_replace( (string) $agents_api_dir . '/', '', $file->getPathname() ) . ' registers admin UI via ' . $function_name; + $matches[] = $relative_path . ' registers admin UI via ' . $function_name; } } foreach ( $forbidden_admin_hooks as $hook_name ) { if ( preg_match( '/add_action\s*\(\s*[\'\"]' . preg_quote( $hook_name, '/' ) . '/', $source ) ) { - $matches[] = str_replace( (string) $agents_api_dir . '/', '', $file->getPathname() ) . ' registers admin hook ' . $hook_name; + $matches[] = $relative_path . ' registers admin hook ' . $hook_name; + } + } + + foreach ( $forbidden_product_vocabulary as $term ) { + if ( preg_match( '/(? $reason ) { + if ( preg_match( $pattern, $source ) ) { + $matches[] = $relative_path . ' ' . $reason; } } } -agents_api_smoke_assert_equals( array(), $matches, 'agents-api has no product imports or admin UI registrations', $failures, $passes ); +agents_api_smoke_assert_equals( array(), $matches, 'agents-api production source has no product imports, product vocabulary, admin UI registrations, or table ownership', $failures, $passes ); agents_api_smoke_assert_equals( $forbidden_namespaces, array_values( array_unique( $forbidden_namespaces ) ), 'forbidden namespace list has no duplicates', $failures, $passes ); +agents_api_smoke_assert_equals( $forbidden_product_vocabulary, array_values( array_unique( $forbidden_product_vocabulary ) ), 'forbidden product vocabulary list has no duplicates', $failures, $passes ); agents_api_smoke_assert_equals( $forbidden_admin_apis, array_values( array_unique( $forbidden_admin_apis ) ), 'forbidden admin API list has no duplicates', $failures, $passes ); agents_api_smoke_assert_equals( $forbidden_admin_hooks, array_values( array_unique( $forbidden_admin_hooks ) ), 'forbidden admin hook list has no duplicates', $failures, $passes );