From ff474326fb82af56674090adc914b8c7cf5e98e9 Mon Sep 17 00:00:00 2001
From: peptiderepo <273788462+peptiderepo@users.noreply.github.com>
Date: Tue, 16 Jun 2026 21:30:24 +0700
Subject: [PATCH] Adopt reusable CI standard (#30)
Squash-merged from claude/adopt-ci-20260616.
QA-approved; all CI checks green on 3b4e3f04.
Agent-Session: deploy-ci-rollout-news-plugin-2026-06-16
---
.github/workflows/ci.yml | 147 +++---------------
.github/workflows/deploy.yml | 6 +-
.github/workflows/security-audit.yml | 58 +++++++
.phpcs.xml.dist | 10 +-
CHANGELOG.md | 30 ++++
composer.json | 6 +-
...lass-peptide-news-content-filter-rules.php | 2 +-
.../class-peptide-news-content-filter.php | 6 +-
includes/class-peptide-news-fetcher.php | 2 +-
includes/class-peptide-news-llm-client.php | 2 +-
includes/class-peptide-news-llm.php | 2 +-
includes/class-peptide-news-rest-api.php | 1 +
.../class-peptide-news-source-resolver.php | 2 +-
peptide-news-plugin.php | 4 +-
phpunit.xml.dist | 5 +-
uninstall.php | 6 +-
16 files changed, 139 insertions(+), 150 deletions(-)
create mode 100644 .github/workflows/security-audit.yml
create mode 100644 CHANGELOG.md
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.