diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 04a6547..4cf6a54 100755 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,134 +1,27 @@ -name: CI — Lint & Test +## +# Thin CI caller — peptide-news-plugin +# +# Delegates all lint/test/file-size jobs to the estate reusable workflow. +# Per estate standard: workflow_call is required so deploy.yml can gate on it. +# See: peptiderepo/peptide-e2e/.github/workflows/ci.yml@main for job definitions. +## +name: CI on: + pull_request: {} push: - branches: [ main ] - pull_request: - branches: [ main ] - workflow_call: + branches: [main] + workflow_call: {} +# Required: calling workflow must declare contents: write so the reusable +# phpcs job can commit phpcbf auto-fixes back to the PR branch. permissions: - contents: read + contents: write jobs: - # ── PHP Syntax Check ───────────────────────────────────────────── - lint-php: - name: PHP Lint (${{ matrix.php }}) - runs-on: ubuntu-latest - strategy: - matrix: - php: ['8.1', '8.2', '8.3'] - steps: - - uses: actions/checkout@v4 - - - name: Set up PHP ${{ matrix.php }} - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php }} - tools: composer - - - name: Syntax check all PHP files - run: find . -name '*.php' -not -path './vendor/*' -print0 | xargs -0 -n1 php -l - - # ── Security Audit ─────────────────────────────────────────────── - security-audit: - name: Security Audit - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Check for common PHP security issues - run: | - echo ":: Checking for direct SQL queries without prepare..." - # Exclude uninstall.php (DROP TABLE is standard WP pattern), activator (table DDL), - # and logger (TRUNCATE) — these use $wpdb->prefix which is safe internal state. - if grep -r "query\s*(\s*['\"].*\$" --include="*.php" --exclude-dir=vendor . 2>/dev/null \ - | grep -v "\\\$wpdb->prepare" \ - | grep -v "uninstall\.php" \ - | grep -v "class-peptide-news-activator\.php" \ - | grep -v "class-peptide-news-logger\.php" \ - | grep -v "DROP TABLE" \ - | grep -v "TRUNCATE" \ - && [ \${PIPESTATUS[0]} -eq 0 ]; then - echo "WARNING: Found potential unescaped SQL queries. Use wpdb->prepare() for all dynamic SQL." - exit 1 - fi - - echo ":: Checking for eval() usage..." - if grep -r "eval\s*(" --include="*.php" --exclude-dir=vendor . 2>/dev/null; then - echo "ERROR: eval() found. This is a critical security risk." - exit 1 - fi - - echo ":: Checking for base64_decode without validation..." - if grep -r "base64_decode\s*(" --include="*.php" --exclude-dir=vendor . 2>/dev/null; then - echo "WARNING: Found base64_decode(). Ensure the input is properly validated." - fi - - echo ":: Checking for file_get_contents with user input..." - if grep -r "file_get_contents\s*(\s*\$" --include="*.php" --exclude-dir=vendor . 2>/dev/null; then - echo "WARNING: Found file_get_contents with variable. Ensure input is validated and restricted." - fi - - echo "All security checks passed." - - # ── PHPCS Coding Standards ─────────────────────────────────────── - phpcs: - name: PHPCS - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Set up PHP - uses: shivammathur/setup-php@v2 - with: - php-version: '8.3' - tools: composer - - - name: Install dependencies - run: composer install --prefer-dist --no-progress - - - name: Run PHPCS - run: vendor/bin/phpcs --standard=.phpcs.xml.dist - continue-on-error: true - - # ── PHPUnit Tests ──────────────────────────────────────────────── - phpunit: - name: PHPUnit (${{ matrix.php }}) - runs-on: ubuntu-latest - strategy: - matrix: - php: ['8.1', '8.2', '8.3'] - steps: - - uses: actions/checkout@v4 - - - name: Set up PHP ${{ matrix.php }} - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php }} - tools: composer - coverage: xdebug - - - name: Install dependencies - run: composer install --prefer-dist --no-progress - - - name: Run PHPUnit - run: vendor/bin/phpunit --testdox - - # ── JavaScript Lint ────────────────────────────────────────────── - lint-js: - name: JS Lint - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Set up Node - uses: actions/setup-node@v4 - with: - node-version: '20' - - - name: Check JS syntax - run: | - for f in $(find . -name '*.js' -not -path './vendor/*' -not -path './node_modules/*' -not -path '*/vendor/*'); do - node --check "$f" || exit 1 - done + ci: + uses: peptiderepo/peptide-e2e/.github/workflows/ci.yml@main + with: + tests: stubs + has_js: true + secrets: inherit diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index b2e104a..9b1233e 100755 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -4,8 +4,12 @@ on: push: branches: [ main ] +# contents: write required so the ci.yml caller can pass write permission +# through to the phpcbf auto-commit step in the reusable workflow. +# (phpcbf commit only fires on pull_request events, but GH enforces caller +# permissions at token-grant time, not at step-execution time.) permissions: - contents: read + contents: write concurrency: group: deploy-peptide-news-plugin diff --git a/.github/workflows/security-audit.yml b/.github/workflows/security-audit.yml new file mode 100644 index 0000000..428411a --- /dev/null +++ b/.github/workflows/security-audit.yml @@ -0,0 +1,58 @@ +## +# Security audit — peptide-news-plugin +# +# Standalone workflow kept separate from the estate CI caller so it can evolve +# independently and run on every PR without being bundled into the reusable pipeline. +# Checks for: unescaped SQL, eval(), base64_decode(), file_get_contents with user input. +## +name: Security Audit + +on: + pull_request: {} + push: + branches: [main] + +permissions: + contents: read + +jobs: + security-audit: + name: Security Audit + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Check for common PHP security issues + run: | + echo ":: Checking for direct SQL queries without prepare..." + # Exclude uninstall.php (DROP TABLE is standard WP pattern), activator (table DDL), + # and logger (TRUNCATE) — these use $wpdb->prefix which is safe internal state. + if grep -r "query\s*(\s*['\"].*\$" --include="*.php" --exclude-dir=vendor . 2>/dev/null \ + | grep -v "\$wpdb->prepare" \ + | grep -v "uninstall\.php" \ + | grep -v "class-peptide-news-activator\.php" \ + | grep -v "class-peptide-news-logger\.php" \ + | grep -v "DROP TABLE" \ + | grep -v "TRUNCATE" \ + && [ ${PIPESTATUS[0]} -eq 0 ]; then + echo "WARNING: Found potential unescaped SQL queries. Use wpdb->prepare() for all dynamic SQL." + exit 1 + fi + + echo ":: Checking for eval() usage..." + if grep -r "eval\s*(" --include="*.php" --exclude-dir=vendor . 2>/dev/null; then + echo "ERROR: eval() found. This is a critical security risk." + exit 1 + fi + + echo ":: Checking for base64_decode without validation..." + if grep -r "base64_decode\s*(" --include="*.php" --exclude-dir=vendor . 2>/dev/null; then + echo "WARNING: Found base64_decode(). Ensure the input is properly validated." + fi + + echo ":: Checking for file_get_contents with user input..." + if grep -r "file_get_contents\s*(\s*\$" --include="*.php" --exclude-dir=vendor . 2>/dev/null; then + echo "WARNING: Found file_get_contents with variable. Ensure input is validated and restricted." + fi + + echo "All security checks passed." diff --git a/.phpcs.xml.dist b/.phpcs.xml.dist index 8bd8ec7..016ceec 100755 --- a/.phpcs.xml.dist +++ b/.phpcs.xml.dist @@ -73,9 +73,13 @@ warning - - - + diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..ba2b9af --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,30 @@ +# Changelog + +All notable changes to Peptide News Plugin are documented here. +Format: [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +Versioning: [Semantic Versioning](https://semver.org/). + +## [2.5.0] — 2026-06-16 + +### Changed +- **CI standardization (estate wave 2):** Replace per-repo CI jobs in + `ci.yml` with a thin caller of `peptiderepo/peptide-e2e/.github/workflows/ci.yml@main` + (`tests: stubs`, `has_js: true`). Adds `workflow_call` trigger so `deploy.yml` + gate remains valid. +- Extract `security-audit` job from `ci.yml` into standalone + `.github/workflows/security-audit.yml` (kept as-is, unique to this repo). +- `deploy.yml`: elevate permissions to `contents: write` so the nested + phpcbf commit-back step in the reusable workflow receives the correct token scope. +- `composer.json`: pin `test` script to `vendor/bin/phpunit --configuration phpunit.xml.dist` + for explicit path resolution in CI; pin `lint` / `lint:fix` to `vendor/bin/`. +- `phpunit.xml.dist`: add XSD reference for PHPUnit 9.6; drop deprecated + `convertErrorsToExceptions`, `convertNoticesToExceptions`, `convertWarningsToExceptions` + attributes. + +### Notes +- `rollback.yml` and `cto-review.yml` and `main-push-audit.yml` unchanged — unique / estate-separate. +- No functional code changes in this commit. + +## Prior history + +Versions prior to 2.5.0 were not tracked in a CHANGELOG. See git log for details. diff --git a/composer.json b/composer.json index eb532a1..d519518 100755 --- a/composer.json +++ b/composer.json @@ -21,9 +21,9 @@ } }, "scripts": { - "test": "phpunit", - "lint": "phpcs", - "lint:fix": "phpcbf", + "test": "vendor/bin/phpunit --configuration phpunit.xml.dist", + "lint": "vendor/bin/phpcs", + "lint:fix": "vendor/bin/phpcbf", "check": [ "@lint", "@test" diff --git a/includes/class-peptide-news-content-filter-rules.php b/includes/class-peptide-news-content-filter-rules.php index 13e091c..d053205 100644 --- a/includes/class-peptide-news-content-filter-rules.php +++ b/includes/class-peptide-news-content-filter-rules.php @@ -107,7 +107,7 @@ class Peptide_News_Content_Filter_Rules { continue; } if ( false !== strpos( $body_text, $keyword ) ) { - $body_matches++; + ++$body_matches; $matched_keys[] = $keyword; $score += 15; } diff --git a/includes/class-peptide-news-content-filter.php b/includes/class-peptide-news-content-filter.php index 98f3828..5fd298f 100755 --- a/includes/class-peptide-news-content-filter.php +++ b/includes/class-peptide-news-content-filter.php @@ -79,7 +79,7 @@ class Peptide_News_Content_Filter { ); if ( 'promotional' === $result['verdict'] ) { - $removed++; + ++$removed; self::log( sprintf( 'Blocked (rule: %s, score: %d): %s', $result['rule'], $result['score'], $article['title'] ) ); continue; } @@ -87,10 +87,10 @@ class Peptide_News_Content_Filter { // Borderline articles go to LLM for a second opinion. if ( 'borderline' === $result['verdict'] && self::is_llm_filter_enabled() ) { $llm_verdict = self::classify_with_llm( $article ); - $llm_checked++; + ++$llm_checked; if ( 'promotional' === $llm_verdict ) { - $removed++; + ++$removed; self::log( sprintf( 'Blocked (LLM confirmed borderline): %s', $article['title'] ) ); continue; } diff --git a/includes/class-peptide-news-fetcher.php b/includes/class-peptide-news-fetcher.php index f41a5d0..23508b2 100755 --- a/includes/class-peptide-news-fetcher.php +++ b/includes/class-peptide-news-fetcher.php @@ -87,7 +87,7 @@ class Peptide_News_Fetcher { $stored = 0; foreach ( $articles as $article ) { if ( $this->store_article( $article ) ) { - $stored++; + ++$stored; } } diff --git a/includes/class-peptide-news-llm-client.php b/includes/class-peptide-news-llm-client.php index 67f99b7..1df1319 100644 --- a/includes/class-peptide-news-llm-client.php +++ b/includes/class-peptide-news-llm-client.php @@ -137,7 +137,7 @@ class Peptide_News_LLM_Client { $retry_after = wp_remote_retrieve_header( $response, 'retry-after' ); $wait = $retry_after ? min( (int) $retry_after, 30 ) : $backoff; sleep( $wait ); - $retries++; + ++$retries; $backoff *= 2; continue; } diff --git a/includes/class-peptide-news-llm.php b/includes/class-peptide-news-llm.php index 57abb61..ee39df2 100755 --- a/includes/class-peptide-news-llm.php +++ b/includes/class-peptide-news-llm.php @@ -183,7 +183,7 @@ class Peptide_News_LLM { $result = self::process_article( $article ); if ( ! empty( $result['success'] ) ) { - $processed++; + ++$processed; } if ( ! empty( $result['errors'] ) ) { $last_errors = $result['errors']; diff --git a/includes/class-peptide-news-rest-api.php b/includes/class-peptide-news-rest-api.php index 80d4e18..57b75a5 100755 --- a/includes/class-peptide-news-rest-api.php +++ b/includes/class-peptide-news-rest-api.php @@ -104,6 +104,7 @@ class Peptide_News_Rest_API { if ( ! preg_match( '/^\\d{4}-\\d{2}-\\d{2}$/', $value ) ) { return new WP_Error( 'rest_invalid_date', + /* translators: %s: REST API parameter name */ sprintf( __( 'Invalid date format for %s. Expected Y-m-d.', 'peptide-news' ), $param ), array( 'status' => 400 ) ); diff --git a/includes/class-peptide-news-source-resolver.php b/includes/class-peptide-news-source-resolver.php index 2a4d1c2..6f31316 100644 --- a/includes/class-peptide-news-source-resolver.php +++ b/includes/class-peptide-news-source-resolver.php @@ -126,7 +126,7 @@ class Peptide_News_Source_Resolver { if ( ! empty( $new_source ) && $new_source !== $article->source ) { $wpdb->update( $table, array( 'source' => $new_source ), array( 'id' => $article->id ), array( '%s' ), array( '%d' ) ); - $updated++; + ++$updated; } } diff --git a/peptide-news-plugin.php b/peptide-news-plugin.php index a9817cc..dfe77bd 100755 --- a/peptide-news-plugin.php +++ b/peptide-news-plugin.php @@ -3,7 +3,7 @@ * Plugin Name: Peptide News Aggregator * Plugin URI: https://github.com/peptiderepo/peptide-news-plugin * Description: Aggregates and displays the latest peptide research news from multiple sources with click analytics and trend reporting. - * Version: 2.4.1 + * Version: 2.5.0 * Author: peptiderepo * Author URI: https://github.com/peptiderepo * License: GPL-2.0+ @@ -24,7 +24,7 @@ if ( ! defined( 'WPINC' ) ) { /** * Current plugin version — follows SemVer. */ -define( 'PEPTIDE_NEWS_VERSION', '2.4.0' ); +define( 'PEPTIDE_NEWS_VERSION', '2.5.0' ); define( 'PEPTIDE_NEWS_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); define( 'PEPTIDE_NEWS_PLUGIN_URL', plugin_dir_url( __FILE__ ) ); define( 'PEPTIDE_NEWS_PLUGIN_BASENAME', plugin_basename( __FILE__ ) ); diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 56d13db..62a2b11 100755 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,11 +1,10 @@ diff --git a/uninstall.php b/uninstall.php index d8e808a..907bc94 100755 --- a/uninstall.php +++ b/uninstall.php @@ -2,7 +2,7 @@ declare( strict_types=1 ); if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) { - exit; + exit; } global $wpdb; $prefix = $wpdb->prefix; @@ -16,12 +16,12 @@ $wpdb->query( "DROP TABLE IF EXISTS {$prefix}peptide_news_articles" ); // Delete all plugin options. $wpdb->query( - "DELETE FROM {$wpdb->options} WHERE option_name LIKE 'peptide\_news\_%'" + "DELETE FROM {$wpdb->options} WHERE option_name LIKE 'peptide\_news\_%'" ); // Remove transient caches. $wpdb->query( - "DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_peptide\_news\_%' OR option_name LIKE '_transient_timeout_peptide\_news\_%'" + "DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_peptide\_news\_%' OR option_name LIKE '_transient_timeout_peptide\_news\_%'" ); // Clear scheduled events.