diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6ad40ba..a8d48a5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -109,3 +109,20 @@ jobs: run: composer install --no-progress --no-interaction --prefer-dist - name: Run PHPUnit (Brain Monkey — no WP install required) run: vendor/bin/phpunit --testdox + + check-constants: + name: Check PRAUTOBLOGGER_* constants + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Setup PHP 8.1 + uses: shivammathur/setup-php@v2 + with: + php-version: '8.1' + coverage: none + - name: Check PRAUTOBLOGGER_* constants + # Fails if any PRAUTOBLOGGER_* constant referenced in the shipped + # plugin PHP is neither defined in prautoblogger.php nor guarded + # by defined() at the reference site. Prevents the v0.19.0 incident + # class (constant deleted from bootstrap, test fallback masks the gap). + run: php scripts/check-constants.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 776ddee..2dfa494 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ All notable changes to PRAutoBlogger will be documented in this file. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project uses [Semantic Versioning](https://semver.org/). +## [Unreleased - Internal] + +### Internal +- CI constants guard (`scripts/check-constants.php`): new required CI step (`Check PRAUTOBLOGGER_* constants`) fails the build when any `PRAUTOBLOGGER_*` constant referenced in shipped plugin PHP is neither defined in `prautoblogger.php` nor `defined()`-guarded at the reference site. Prevents a recurrence of the v0.19.0 admin-500 regression class. + ## [0.20.1] - 2026-06-12 ### Fixed diff --git a/scripts/check-constants.php b/scripts/check-constants.php new file mode 100644 index 0000000..6b0b9b6 --- /dev/null +++ b/scripts/check-constants.php @@ -0,0 +1,212 @@ + $defined_set constant name => 1 */ +$defined_set = array_flip( $def_matches[1] ?? [] ); + +if ( $verbose ) { + echo 'Defined in ' . CONSTANTS_BOOTSTRAP_FILE . ' (' . count( $defined_set ) . "):\n"; + foreach ( array_keys( $defined_set ) as $constant_name ) { + echo " {$constant_name}\n"; + } + echo "\n"; +} + +// ------------------------------------------------------------------------- +// 2. Collect shipped PHP files to scan. +// ------------------------------------------------------------------------- + +/** + * Recursively collect *.php paths under a directory, skipping excluded dirs. + * + * @param string $dir Directory to walk. + * @param string[] $files Accumulator (modified by reference). + * @return void + */ +function constants_collect_php_files( string $dir, array &$files ): void { + $dir_iter = new DirectoryIterator( $dir ); + foreach ( $dir_iter as $entry ) { + if ( $entry->isDot() ) { + continue; + } + $basename = $entry->getBasename(); + if ( $entry->isDir() ) { + if ( ! in_array( $basename, CONSTANTS_EXCLUDE_DIRS, true ) ) { + constants_collect_php_files( $entry->getPathname(), $files ); + } + continue; + } + if ( $entry->isFile() && $entry->getExtension() === 'php' ) { + $files[] = $entry->getPathname(); + } + } +} + +/** @var string[] $php_files */ +$php_files = []; + +foreach ( CONSTANTS_SCAN_ROOTS as $root ) { + if ( ! file_exists( $root ) ) { + // Template dir may not exist in very early checkouts -- skip silently. + continue; + } + if ( is_file( $root ) ) { + $php_files[] = $root; + } else { + constants_collect_php_files( $root, $php_files ); + } +} + +sort( $php_files ); + +if ( $verbose ) { + echo 'Scanning ' . count( $php_files ) . " PHP files\n\n"; +} + +// ------------------------------------------------------------------------- +// 3. Scan each file for bare PRAUTOBLOGGER_* references. +// ------------------------------------------------------------------------- + +/** @var array $errors */ +$errors = []; + +foreach ( $php_files as $php_path ) { + $raw = file_get_contents( $php_path ); + if ( $raw === false ) { + fwrite( STDERR, "WARNING: cannot read {$php_path} -- skipping\n" ); + continue; + } + + $lines = explode( "\n", $raw ); + + foreach ( $lines as $idx => $raw_line ) { + // Fast pre-check: skip lines with no PRAUTOBLOGGER_ token. + if ( strpos( $raw_line, 'PRAUTOBLOGGER_' ) === false ) { + continue; + } + + // Skip pure comment lines (docblock * lines, // comments, # lines). + $trimmed = ltrim( $raw_line ); + if ( + str_starts_with( $trimmed, '*' ) || + str_starts_with( $trimmed, '//' ) || + str_starts_with( $trimmed, '#' ) + ) { + continue; + } + + // Extract all constant names from this line. + if ( ! preg_match_all( '/PRAUTOBLOGGER_[A-Z0-9_]+/', $raw_line, $match ) ) { + continue; + } + + foreach ( $match[0] as $name ) { + // Skip if this line is itself a define() call for this name. + if ( preg_match( "/define\s*\(\s*'" . preg_quote( $name, '/' ) . "'/", $raw_line ) ) { + continue; + } + + // Skip if the constant is production-defined in prautoblogger.php. + if ( isset( $defined_set[ $name ] ) ) { + continue; + } + + // Check for a defined() guard in the preceding GUARD_WINDOW lines + // (inclusive of the current line to catch same-line chained &&). + $window_start = max( 0, $idx - CONSTANTS_GUARD_WINDOW ); + $guarded = false; + for ( $w = $window_start; $w <= $idx; $w++ ) { + if ( preg_match( "/defined\s*\(\s*'" . preg_quote( $name, '/' ) . "'/", $lines[ $w ] ) ) { + $guarded = true; + break; + } + } + + if ( ! $guarded ) { + $errors[] = [ $php_path, $idx + 1, $name, $raw_line ]; + } + } + } +} + +// ------------------------------------------------------------------------- +// 4. Report results. +// ------------------------------------------------------------------------- + +if ( $errors !== [] ) { + fwrite( STDERR, "\nFAIL -- Unguarded undefined PRAUTOBLOGGER_* constants:\n\n" ); + foreach ( $errors as [ $php_path, $lineno, $name, $line_text ] ) { + fwrite( STDERR, " {$php_path}:{$lineno}: {$name}\n" ); + fwrite( STDERR, ' ' . trim( $line_text ) . "\n" ); + } + fwrite( STDERR, "\nTo fix: add define( '" . $errors[0][2] . "', ... ) to prautoblogger.php,\n" ); + fwrite( STDERR, "or guard the reference with if ( defined( '" . $errors[0][2] . "' ) ) { ... }\n\n" ); + exit( 1 ); +} + +echo "OK -- All PRAUTOBLOGGER_* constants are production-defined or defined()-guarded.\n"; +exit( 0 );