ci: add PRAUTOBLOGGER_* constants guard (scripts/check-constants.php)
Wires a new required CI step — 'Check PRAUTOBLOGGER_* constants' — that fails the build when any PRAUTOBLOGGER_* constant referenced in the shipped plugin PHP is neither: (a) define()d in prautoblogger.php (including conditional OPIK defines), nor (b) protected by a defined() guard at the reference site. This closes the regression class that caused the v0.19.0 admin-500 incident: PR #156 deleted three constants from prautoblogger.php; CI stayed green because tests/bootstrap.php holds fallback copies, masking the gap until prod boot. Deliverables: - scripts/check-constants.php (212 lines, no Composer deps, PHP 8.1+) - .github/workflows/ci.yml: new 'check-constants' job - CHANGELOG.md: one-line note under [Unreleased - Internal] No plugin version bump (CI-only; shipped artifact unchanged). AC-1 verified locally (Python3 equivalent logic): - HEAD passes clean (0 violations) - Remove PRAUTOBLOGGER_DEFAULT_RUN_CEILING_USD -> 2 violations caught - Remove PRAUTOBLOGGER_DEFAULT_REQUEST_JSON_RETENTION_DAYS -> 3 violations caught - Remove PRAUTOBLOGGER_DEFAULT_REASONING_MAX_TOKENS -> 1 violation caught Agent-Session: ci-constants-guard-20260614
This commit is contained in:
17
.github/workflows/ci.yml
vendored
17
.github/workflows/ci.yml
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
212
scripts/check-constants.php
Normal file
212
scripts/check-constants.php
Normal file
@@ -0,0 +1,212 @@
|
||||
<?php
|
||||
/**
|
||||
* CI guard: every PRAUTOBLOGGER_* constant referenced in the shipped plugin PHP
|
||||
* must be either (a) defined in prautoblogger.php, or (b) protected by a
|
||||
* defined() guard at the reference site.
|
||||
*
|
||||
* Purpose: prevent the class of regression that caused the v0.19.0 admin-500
|
||||
* incident — a constant deleted from prautoblogger.php while tests/bootstrap.php
|
||||
* kept a fallback copy, masking the gap until prod boot.
|
||||
*
|
||||
* Usage (from repo root):
|
||||
* php scripts/check-constants.php [--verbose]
|
||||
*
|
||||
* Exit 0 = all clear. Exit 1 = unguarded undefined constants found.
|
||||
*
|
||||
* Include/exclude summary (relative to repo root):
|
||||
* Defined-set source prautoblogger.php only
|
||||
* Scanned for refs prautoblogger.php, includes/, templates/, uninstall.php
|
||||
* Excluded from refs tests/ vendor/ .github/ .git/ node_modules/
|
||||
* Excluded from defs everything except prautoblogger.php
|
||||
*
|
||||
* Guard window: a bare constant use is considered guarded if a defined() call
|
||||
* for the same constant appears within GUARD_WINDOW lines above it in the same
|
||||
* file (covers if/elseif blocks and multi-line && chains).
|
||||
*
|
||||
* @package PRAutoBlogger
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
// How many lines above the bare use to look for a defined() guard.
|
||||
const CONSTANTS_GUARD_WINDOW = 4;
|
||||
|
||||
// Source of truth for define()d constants -- test-bootstrap fallbacks excluded.
|
||||
const CONSTANTS_BOOTSTRAP_FILE = 'prautoblogger.php';
|
||||
|
||||
// Directories skipped when walking the shipped tree for references.
|
||||
const CONSTANTS_EXCLUDE_DIRS = ['tests', 'vendor', '.github', '.git', 'node_modules'];
|
||||
|
||||
// Entry points for the reference scan (files and dirs relative to repo root).
|
||||
const CONSTANTS_SCAN_ROOTS = [
|
||||
'prautoblogger.php',
|
||||
'uninstall.php',
|
||||
'includes',
|
||||
'templates',
|
||||
];
|
||||
|
||||
$verbose = in_array( '--verbose', $argv ?? [], true );
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 1. Build the defined set from prautoblogger.php only.
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
if ( ! file_exists( CONSTANTS_BOOTSTRAP_FILE ) ) {
|
||||
fwrite( STDERR, "ERROR: bootstrap file not found -- run from repo root.\n" );
|
||||
fwrite( STDERR, ' Expected: ' . CONSTANTS_BOOTSTRAP_FILE . "\n" );
|
||||
exit( 1 );
|
||||
}
|
||||
|
||||
$bootstrap_src = (string) file_get_contents( CONSTANTS_BOOTSTRAP_FILE );
|
||||
|
||||
preg_match_all(
|
||||
"/define\s*\(\s*'(PRAUTOBLOGGER_[A-Z0-9_]+)'/",
|
||||
$bootstrap_src,
|
||||
$def_matches
|
||||
);
|
||||
|
||||
/** @var array<string, int> $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<int, array{string, int, string, string}> $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 );
|
||||
Reference in New Issue
Block a user