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
44 changes: 18 additions & 26 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
84 changes: 71 additions & 13 deletions tests/no-product-imports-smoke.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php
/**
* Static smoke test proving Agents API stays product-free and UI-free.
* Static smoke test proving Agents API stays product-free, UI-free, and table-free.
*
* Run with: php tests/no-product-imports-smoke.php
*
Expand All @@ -17,6 +17,11 @@
$agents_api_dir = realpath( __DIR__ . '/..' );
agents_api_smoke_assert_equals( true, is_string( $agents_api_dir ), 'agents-api directory exists', $failures, $passes );

$production_paths = array(
'agents-api.php' => $agents_api_dir . '/agents-api.php',
'src' => $agents_api_dir . '/src',
);

$forbidden_namespaces = array(
'ExampleProduct\\Core\\Steps',
'ExampleProduct\\Core\\Database\\Jobs',
Expand All @@ -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',
Expand All @@ -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( '/(?<![A-Za-z0-9_])' . preg_quote( $term, '/' ) . '(?![A-Za-z0-9_])/i', $source ) ) {
$matches[] = $relative_path . ' contains downstream product vocabulary: ' . $term;
}
}

foreach ( $forbidden_table_ownership_patterns as $pattern => $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 );

Expand Down