सामग्री पर जाएं
smallPict

Engineering21 min read

WordPress प्लगइन CI/CD: WPCS, PHPStan और PHP 7.4 से 8.5

GitHub Actions में WordPress प्लगइन CI/CD: PHP 7.4 से 8.5 पर WPCS, PHPStan और ऑटोमेटेड टेस्ट, और WordPress.org पर डिप्लॉय से पहले का गेट।

smallPict Engineering

Engineering

smallPict का वर्डप्रेस प्लगइन अपलोड की गई छवियों को WebP और AVIF में बदलता है और उन्हें विज़िटरों को सर्व करता है। जब हम कोई रिलीज़ टैग करते हैं, तो GitHub Actions उसे WordPress.org के SVN रिपॉजिटरी में commit करता है, और वहाँ से वह हर उस साइट तक पहुँचती है जिस पर प्लगइन इंस्टॉल है। जिन साइटों पर ऑटो-अपडेट चालू है, वे बदलाव की किसी समीक्षा के बिना उसे इंस्टॉल कर लेती हैं। प्लगइन Requires PHP: 7.4 और Requires at least: 5.9 घोषित करता है, इसलिए एक ही बिल्ड को PHP 7.4 पर चल रहे शेयर्ड होस्ट पर भी चलना होता है और PHP 8.5 वाले कंटेनर पर भी।

प्लगइन में एक fatal error पूरे wp-admin को ठप कर सकता है। सेटिंग्स पेज पर एक unescaped वैल्यू एक साथ हर साइट पर XSS बग बन जाती है। nonce जाँच छूट जाए तो कोई एडमिन एक्शन CSRF का निशाना बन जाता है। इनमें से किसी को भी चुपचाप पैच नहीं किया जा सकता: फ़िक्स के लिए नई रिलीज़ चाहिए, और कुछ साइटें उसके आने तक टूटा हुआ संस्करण चलाती रहती हैं।

यह पोस्ट हमारी वर्डप्रेस प्लगइन CI/CD पाइपलाइन का वर्णन करती है: WordPress Coding Standards (WPCS), PHPStan और ऑटोमेटेड टेस्ट जो PHP के सात संस्करणों पर चलते हैं, और WordPress.org से पहले का डिप्लॉय गेट। नीचे की हर बात रिलीज़ 1.2.9 पर smallpict-wp रिपॉजिटरी से ली गई है।

मुख्य बातें

  • master में आने वाला हर pull request GitHub Actions में PHP 7.4, 8.0, 8.1, 8.2, 8.3, 8.4 और 8.5 पर समानांतर रूप से WPCS, PHPStan और 9 ऑटोमेटेड टेस्ट स्क्रिप्ट चलाता है।
  • WPCS sanitized इनपुट, escaped आउटपुट, nonce और prepared क्वेरी लागू करवाता है। लेवल 5 पर PHPStan वर्डप्रेस stubs के आधार पर, वर्डप्रेस चलाए बिना, टाइप एरर और डेड ब्रांच पकड़ता है।
  • रिलीज़ टैग सातों PHP संस्करणों पर WPCS फिर से चलाता है, और WordPress.org SVN में commit करने वाला job तभी शुरू होता है जब सातों पास हो जाएँ।

वर्डप्रेस प्लगइन CI/CD पाइपलाइन एक नज़र में

प्लगइन की सुरक्षा दो GitHub Actions workflow करते हैं।

Workflowकब चलता हैJobsजाँचें
phpcs.yml (PHP Code Quality & Standards)master या develop पर push, master में pull requestहर PHP संस्करण के लिए एक job: 7.4, 8.0, 8.1, 8.2, 8.3, 8.4, 8.5WPCS, PHPStan, ऑटोमेटेड टेस्ट
deploy-all.yml (Deploy to WordPress.org)v* टैग का push7 PHP संस्करणों में से हर एक पर WPCS, फिर deployWPCS, फिर बिल्ड और SVN commit

PHP संस्करण कवरेज पर नोट: इस पोस्ट के स्क्रीनशॉट हमारे पहले के CI रन दिखाते हैं, जो PHP 7.4 से 8.3 तक को कवर करते थे। आज हमारा GitHub Actions टेस्ट matrix बढ़कर PHP 7.4 से PHP 8.5 तक को कवर करता है, जिससे आधुनिक PHP रनटाइम के साथ लगातार forward-compatibility बनी रहती है और पुराने एनवायरनमेंट पर कोई regression नहीं आता।

सारे टूल Composer की dev dependency हैं, इसलिए इनमें से कुछ भी उपयोगकर्ताओं तक नहीं पहुँचता:

JSON
"require-dev": {    "wp-coding-standards/wpcs": "^3.4.1",    "phpcsstandards/php_codesniffer": "^3.0",    "phpstan/phpstan": "^2.1",    "szepeviktor/phpstan-wordpress": "^2.0 || ^1.3.5",    "phpstan/extension-installer": "^1.4"}

GitHub Actions में कई PHP संस्करणों पर matrix टेस्टिंग

PHP 7.4 और PHP 8.5 के बीच क्या बदलता है

PHP 7.4 और PHP 8.5 प्लगइन के एक ही सोर्स कोड को अलग-अलग तरह से चलाते हैं। वर्डप्रेस प्लगइन कोड पर असर डालने वाले कुछ बदलाव:

  • PHP 8.0 में कई internal फ़ंक्शन गलत टाइप का आर्ग्युमेंट मिलने पर TypeError फेंकने लगे, जबकि 7.4 चेतावनी के साथ null लौटाता था। जो कोड strlen() को array पास करता था, वह 7.4 पर चलता रहा और 8.0 पर रुक जाता है।
  • PHP 8.0 ने संख्या और non-numeric string की तुलना का तरीका बदल दिया। 0 == "abc" 7.4 पर true है और 8.0 पर false, जिससे option वैल्यू पर कुछ loose तुलनाओं का नतीजा चुपचाप उलट जाता है।
  • PHP 8.0 ने str_starts_with() और उससे जुड़े फ़ंक्शन जोड़े। हमारा इमेज प्रोसेसर str_starts_with() को कॉल करता है। PHP 7.4 पर यह सिर्फ़ इसलिए काम करता है क्योंकि वर्डप्रेस 5.9 और उसके बाद के संस्करणों में इसका polyfill आता है, और हमारा Requires at least: 5.9 इसकी गारंटी देता है।
  • PHP 8.1 ने internal फ़ंक्शनों के non-nullable पैरामीटर में null पास करने को deprecated कर दिया। strtolower( wp_parse_url( $url, PHP_URL_HOST ) ) 7.4 पर ठीक है, लेकिन 8.1 पर जब $url relative हो और host null हो, तो यह deprecation लॉग करता है। कुछ होस्ट deprecation संदेश पेज में ही प्रिंट कर देते हैं।
  • PHP 8.2 ने dynamic properties को deprecated कर दिया। अपनी ही किसी क्लास पर अघोषित property को वैल्यू असाइन करने से हर रिक्वेस्ट पर deprecation लॉग होता है।

इनमें से हर समस्या तभी सामने आती है जब कोड का विश्लेषण या निष्पादन उस PHP संस्करण पर हो जो अलग व्यवहार करता है। इसीलिए पाइपलाइन master में आने वाले हर pull request को एक साथ सातों संस्करणों पर चलाती है।

GitHub Actions matrix

GitHub Actions में WordPress प्लगइन CI रन: PHP 7.4, 8.0, 8.1, 8.2 और 8.3 पर WPCS और PHPStan के पाँच समानांतर jobs के साथ PHP matrix टेस्टिंग, सभी पास

चित्र 1: 1.2.0 के काम को master में merge करने पर चला PHP Code Quality & Standards workflow। हर PHP संस्करण का अपना job है, और PHP 8.3 वाला job वे steps दिखाता है जो हर job चलाता है: PHPCS, PHPStan, फिर ऑटोमेटेड टेस्ट।

PHP संस्करण कवरेज पर नोट: ऊपर के स्क्रीनशॉट हमारे पहले के CI रन दिखाते हैं, जो PHP 7.4 से 8.3 तक को कवर करते थे। आज हमारा GitHub Actions टेस्ट matrix बढ़कर PHP 7.4 से PHP 8.5 तक को कवर करता है, जिससे आधुनिक PHP रनटाइम के साथ लगातार forward-compatibility बनी रहती है और पुराने एनवायरनमेंट पर कोई regression नहीं आता।

YAML
jobs:  code-quality:    name: WPCS & PHPStan (PHP ${{ matrix.php-version }})    runs-on: ubuntu-26.04    strategy:      matrix:        php-version: ['7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5']    steps:      - uses: actions/checkout@v7      - uses: shivammathur/setup-php@v2        with:          php-version: ${{ matrix.php-version }}          tools: composer:v2, cs2pr          ini-values: memory_limit=2G          coverage: none      - run: composer install --prefer-dist --no-progress --no-interaction      - name: Run PHPCS (WordPress Coding Standards)        run: vendor/bin/phpcs -q --standard=phpcs.xml.dist --report=checkstyle | cs2pr      - name: Run PHPStan (WordPress Static Analysis)        run: vendor/bin/phpstan analyse --memory-limit=2G      - name: Run the standalone tests        run: |          for t in tests/test_*.php; do            echo "== $t"            php "$t" || exit 1          done

fail-fast अपने डिफ़ॉल्ट true पर रहता है, इसलिए जब एक संस्करण फ़ेल होता है तो GitHub अभी चल रहे jobs रद्द कर देता है। इससे टूटे हुए pull request की लागत कम रहती है, बदले में एक ही रन में हर संस्करण का नतीजा नहीं दिखता।

matrix क्या साबित करता है, और क्या नहीं

तीनों जाँचों को matrix से बराबर फ़ायदा नहीं मिलता। PHPStan कोड का विश्लेषण उसी PHP संस्करण के हिसाब से करता है जिस पर वह चलता है, और टेस्ट कोड को उसी संस्करण पर चलाते हैं, इसलिए दोनों हर job में अलग जवाब दे सकते हैं। PHPCS tokens पर काम करता है और व्यवहार में सातों पर एक ही जवाब देता है; matrix में यह मुख्य रूप से साबित करता है कि टूलचेन हर संस्करण पर इंस्टॉल होकर चलती है। सिंटैक्स और फ़ंक्शनों को PHP संस्करणों की एक रेंज के विरुद्ध एक ही बार में जाँचना PHPCompatibility का काम है, जो हमारे ruleset में अभी शामिल नहीं है।

WordPress Coding Standards (WPCS) को ऑटोमेट करना

प्लगइन के लिए अहम सुरक्षा sniffs

WPCS वर्डप्रेस के लिए लिखे गए PHP_CodeSniffer sniffs का एक सेट है। प्लगइन सुरक्षा के लिए सबसे अहम नियम WordPress ruleset में ही हैं:

  • WordPress.Security.ValidatedSanitizedInput $_POST, $_GET, $_REQUEST या $_COOKIE से ऐसी हर रीड को फ़्लैग करता है जिसे unslash और sanitize न किया गया हो।
  • WordPress.Security.EscapeOutput ऐसी वैल्यू के हर echo या print को फ़्लैग करता है जो esc_html(), esc_attr() या esc_url() जैसे escaping फ़ंक्शन से न गुज़री हो।
  • WordPress.Security.NonceVerification उस handler को फ़्लैग करता है जो nonce सत्यापित किए बिना रिक्वेस्ट डेटा पढ़ता है।
  • WordPress.DB.PreparedSQL और PreparedSQLPlaceholders उस क्वेरी को फ़्लैग करते हैं जो $wpdb->prepare() के बजाय string concatenation से बनी हो, और उन placeholders को भी जो अपने आर्ग्युमेंट से मेल नहीं खाते।

हमारा WPCS ruleset

phpcs.xml.dist WordPress-Extra और WordPress-Docs जोड़ता है, अनुवाद वाले sniffs के लिए text domain तय करता है, और हर global फ़ंक्शन, क्लास और constant पर हमारा prefix अनिवार्य करता है, ताकि प्लगइन के नाम उसी साइट पर किसी दूसरे प्लगइन के नामों से न टकराएँ:

XML
<rule ref="WordPress">    <exclude name="Universal.Arrays.DisallowShortArraySyntax"/></rule><rule ref="WordPress-Extra"/><rule ref="WordPress-Docs"/>
<rule ref="WordPress.WP.I18n">    <properties>        <property name="text_domain" type="array">            <element value="smallpict"/>        </property>    </properties></rule>
<rule ref="WordPress.NamingConventions.PrefixAllGlobals">    <properties>        <property name="prefixes" type="array">            <element value="smallpict"/>            <element value="SmallPict"/>            <element value="SMALLPICT"/>        </property>    </properties></rule>

WPCS पास करने वाला AJAX handler

प्लगइन का यह handler offload किए गए मीडिया को batches में restore करता है। इनपुट की हर सीमा इसकी पहली पंक्तियों में संभाली गई है:

PHP
public function ajax_restore_offloaded() {    if ( ! check_ajax_referer( 'smallpict_restore_offloaded', 'nonce', false ) || ! current_user_can( 'manage_options' ) ) {        wp_send_json_error( array( 'message' => 'forbidden' ), 403 );    }    global $wpdb;    $after = isset( $_POST['after'] ) ? absint( wp_unslash( $_POST['after'] ) ) : 0;    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- admin action, the rows change on every call.    $ids = $wpdb->get_col(        $wpdb->prepare(            "SELECT DISTINCT post_id FROM {$wpdb->postmeta} WHERE meta_key = %s AND meta_value = %s AND post_id > %d ORDER BY post_id ASC LIMIT %d",            '_smallpict_offloaded',            '1',            $after,            self::RESTORE_BATCH        )    );    // ...}

nonce और capability की जाँच साथ होती है, क्योंकि वैध nonce यह साबित करता है कि रिक्वेस्ट हमारे एडमिन पेज से आई, यह नहीं कि उपयोगकर्ता को वह एक्शन करने की अनुमति है। $_POST['after'] को क्वेरी तक पहुँचने से पहले unslash करके absint() से cast किया जाता है, और वैल्यू पहले से integer होने के बावजूद क्वेरी फिर भी $wpdb->prepare() से होकर जाती है।

suppression के लिए sniff का नाम और कारण ज़रूरी है

ऊपर वाली phpcs:ignore पंक्ति जानबूझकर रखी गई है। WPCS object caching के बिना सीधी डेटाबेस क्वेरी पर चेतावनी देता है, जो front-end रीड के लिए अच्छी सलाह है और ऐसे एडमिन एक्शन के लिए गलत, जिसकी rows हर कॉल पर बदलती हैं। हमारा नियम है कि हर ignore ठीक-ठीक sniff का नाम ले और -- के बाद कारण बताए। सिर्फ़ // phpcs:ignore लिखा हो तो वह review में पास नहीं होता।

CI में PHPCS एक checkstyle रिपोर्ट लिखता है और cs2pr उसे pull request के diff पर annotations में बदल देता है, ताकि reviewer हर उल्लंघन को लॉग में खोजने के बजाय उसी पंक्ति पर देखे जिसने उसे पैदा किया।

PHPStan स्टैटिक एनालिसिस से लॉजिक की खामियाँ पकड़ना

WPCS कोड को पंक्ति दर पंक्ति पढ़ता है। उसे पता है कि किसी echo में escaping कॉल नहीं है, लेकिन यह नहीं कि किसी वेरिएबल में किस टाइप की वैल्यू है या कोई ब्रांच कभी चल भी सकती है या नहीं। PHPStan हर फ़ंक्शन से होकर गुज़रने वाले टाइप का एक मॉडल बनाता है और उस कोड की रिपोर्ट करता है जो इस मॉडल से मेल नहीं खाता: ऐसा method कॉल करना जो मौजूद नहीं, जहाँ string चाहिए वहाँ string|false पास करना, ऐसी वैल्यू की तुलना जो कभी बराबर नहीं हो सकतीं, ऐसी शर्तें जो हमेशा true या हमेशा false हों।

वर्डप्रेस चलाए बिना वर्डप्रेस के साथ PHPStan का उपयोग

szepeviktor/phpstan-wordpress stub फ़ाइलों के ज़रिए PHPStan को वर्डप्रेस core के फ़ंक्शनों और क्लासों के signatures देता है, साथ में वर्डप्रेस के लिए कुछ खास type extensions भी। phpstan/extension-installer इसे अपने-आप रजिस्टर कर देता है। PHPStan इन declarations को पढ़ता है और कभी वर्डप्रेस या डेटाबेस लोड नहीं करता।

हमारा PHPStan कॉन्फ़िगरेशन

YAML
parameters:    level: 5    paths:        - smallpict.php        - includes/        - admin/    treatPhpDocTypesAsCertain: false    bootstrapFiles:        - phpstan-bootstrap.php

लेवल 5 निचले लेवलों की जाँचों (अज्ञात क्लास, method और property, return type और डेड कोड) के ऊपर, किसी फ़ंक्शन या method को पास किए गए हर आर्ग्युमेंट का टाइप जाँचता है। treatPhpDocTypesAsCertain: false PHPStan से कहता है कि हमारे अपने @param docblocks को सबूत न माने। वर्डप्रेस वही पास करता है जो कोई फ़िल्टर लौटाए, इसलिए int के रूप में दस्तावेज़ित पैरामीटर किसी दूसरे प्लगइन से string बनकर भी आ सकता है, और हम चाहते हैं कि PHPStan उन defensive जाँचों को बना रहने दे जो इस स्थिति को संभालती हैं। bootstrap फ़ाइल प्लगइन के constants (SMALLPICT_PATH, SMALLPICT_URL और अन्य) परिभाषित करती है, जो सामान्यतः रनटाइम पर सेट होते हैं।

GitHub Actions में PHPStan WordPress स्टैटिक एनालिसिस लॉग: WPCS step साफ़, PHPStan ने प्लगइन की 11 में से 11 फ़ाइलों का विश्लेषण किया, नतीजा "[OK] No errors"

चित्र 2: PHP 8.3 job के PHPCS और PHPStan steps। रिपोर्ट करने को कुछ न हो तो PHPCS कुछ प्रिंट नहीं करता। PHPStan प्लगइन की 11 फ़ाइलों (मुख्य प्लगइन फ़ाइल और includes/ की 10 क्लासें) का विश्लेषण करता है और कोई एरर रिपोर्ट नहीं करता।

PHPStan की एक असली खोज: इमेज प्रोसेसर की पंक्ति 626

2 अक्टूबर 2026 को पाइपलाइन PHPStan पर एक एरर के साथ फ़ेल हुई:

WordPress प्लगइन में बग पकड़ता PHPStan: includes/class-smallpict-image-processor.php की पंक्ति 626 पर "Right side of && is always true", नियम booleanAnd.rightAlwaysTrue, PHP 8.2 पर job फ़ेल

चित्र 3: PHP 8.2 job, PHPStan step पर includes/class-smallpict-image-processor.php की पंक्ति 626 पर booleanAnd.rightAlwaysTrue के साथ रुक गया। उस समय विश्लेषित paths में प्लगइन की 9 फ़ाइलें थीं।

यह कोड कन्वर्ज़न का आखिरी चरण है। जब प्लगइन की आउटपुट फ़ॉर्मेट सेटिंग WebP या AVIF हो, तो कन्वर्ट की गई फ़ाइल attachment बन जाती है और डिस्क स्पेस बचाने के लिए मूल अपलोड हटा दिया जाता है। बाकी दो विकल्प, "AVIF, बैकअप के रूप में WebP के साथ" और "मूल फ़ॉर्मेट रखें", मूल JPEG और PNG फ़ाइलें रखते हैं। यह डिलीट वाली ब्रांच थी:

PHP
// C. Pure 1-to-1 Mode: Delete raw uncompressed source file to free 100% hosting space.if ( ( $is_single_webp || $is_single_avif ) && $canonical_path !== $main_file_path && file_exists( $main_file_path ) ) {    wp_delete_file( $main_file_path );}

PHPStan का दावा था कि उस बिंदु पर file_exists( $main_file_path ) हमेशा true है। इसका कारण 150 पंक्ति पहले, उसी method की शुरुआत में है:

PHP
$main_file_path = get_attached_file( $attachment_id );if ( ! $main_file_path || ! file_exists( $main_file_path ) ) {    return new WP_Error( 'file_missing', 'File not found on disk.' );}

method तभी आगे बढ़ता है जब फ़ाइल मौजूद हो, और PHPStan यह तथ्य पंक्ति 626 तक साथ ले जाता है।

हमने इस खोज को आँख मूँदकर नहीं माना। file_exists() फ़ाइल सिस्टम से पूछता है, और दो कॉल के बीच जवाब बदल सकता है। PHPStan डिफ़ॉल्ट रूप से ऐसे कॉल का नतीजा याद रखता है (rememberPossiblyImpureFunctionValues विकल्प), इस मान्यता पर कि बीच में डिस्क पर कुछ नहीं बदला। जाँच हटाने से पहले हमने कोड पढ़कर दो बातें पक्की कीं:

  1. guard और पंक्ति 626 के बीच कुछ भी सोर्स फ़ाइल को न हटाता है, न खिसकाता है। कन्वर्ज़न हर आउटपुट को एक अस्थायी फ़ाइल में लिखता है और उसे नए path पर ले जाता है, और उस path पर wp_delete_file() के जो भी कॉल हैं, वे केवल अस्थायी फ़ाइलें हटाते हैं। offload चरण, जो सचमुच लोकल फ़ाइलें हटाता है, पंक्ति 626 के बाद चलता है।
  2. wp_delete_file() वैसे भी गायब फ़ाइल को झेल लेता है। यह path को wp_delete_file फ़िल्टर से गुज़ारता है और @unlink() कॉल करता है, जो फ़ाइल पहले से हट चुकी हो तो कोई नुकसान नहीं करता।

दोनों बातें पक्की होने पर वह शर्त डेड कोड थी, और डिलीट वाली ब्रांच में डेड कोड अगले पाठक को इस भ्रम में डालता है कि फ़ाइल method के बीच में गायब हो सकती है। फ़िक्स ने उसे हटा दिया:

DIFF
-if ( ( $is_single_webp || $is_single_avif ) && $canonical_path !== $main_file_path && file_exists( $main_file_path ) ) {+if ( ( $is_single_webp || $is_single_avif ) && $canonical_path !== $main_file_path ) {     wp_delete_file( $main_file_path ); }

यह खोज कोई क्रैश नहीं थी। इसका मूल्य उस बात में था जो इसने method के बारे में बताई: कोड की मान्यताएँ उसकी संरचना से दूर हो चुकी थीं, और वह भी उस इकलौती ब्रांच में जो उपयोगकर्ताओं की फ़ाइलें हटाती है। हमने जो सबक लिया वह "analyser पर भरोसा करो" से ज़्यादा सीमित है। फ़ाइल सिस्टम, नेटवर्क या समय से जुड़े किसी फ़ंक्शन पर हमेशा-true या हमेशा-false की रिपोर्ट मिले, तो शर्त हटाने से पहले दोनों कॉल के बीच क्या होता है, इसकी हाथ से जाँच होनी चाहिए।

PHPUnit के बिना वर्डप्रेस प्लगइन की ऑटोमेटेड टेस्टिंग

हर matrix job का आखिरी step tests/ की फ़ाइलें चलाता है। ये PHPUnit टेस्ट नहीं हैं। हर फ़ाइल एक सादी PHP स्क्रिप्ट है जो अपनी ज़रूरत के वर्डप्रेस फ़ंक्शनों (get_option(), update_option(), sanitize_key() आदि) के न्यूनतम stubs परिभाषित करती है, टेस्ट होने वाली प्लगइन क्लास लोड करती है, उसे चलाती है और पहली विफलता पर non-zero कोड के साथ exit करती है:

Shell
for t in tests/test_.php do  echo "== $t"  php "$t"  exit 1done

आज ऐसी 9 स्क्रिप्ट हैं। ये उन paths को कवर करती हैं जहाँ एक गलती ग्राहक का डेटा या पैसा ले डूबती है: WebP और AVIF कन्वर्ज़न पाइपलाइन और उसके fallbacks, इमेज डिलीवरी, क्लाउड स्टोरेज में फ़ाइलों को offload करना और हटाना, offload की गई फ़ाइलों को batches में restore करना, किसी अकाउंट का भुगतान बंद होने पर व्यवहार, स्टोरेज कोटा, और uninstall। uninstall टेस्ट असली uninstall क्वेरी को in-memory SQLite डेटाबेस पर चलाता है और जाँचता है कि क्या बचा रहा:

PHP
check(    array( '_smallpict_cdn_base', '_smallpict_cdn_key', '_smallpict_offloaded', '_smallpictx', '_wp_attached_file' ) === $meta,    'offload markers and foreign meta remain (got ' . implode( ',', $meta ) . ')');

यह assertion एक ऐसे फ़ैसले की रक्षा करता है जिसे गलती से पलट देना आसान है। प्लगइन uninstall करने पर उसकी सेटिंग्स हट जाती हैं, लेकिन वह post meta बना रहता है जो दर्ज करता है कि कौन-सी फ़ाइलें केवल क्लाउड स्टोरेज में हैं, ताकि प्लगइन दोबारा इंस्टॉल करने वाली साइट अपनी छवियाँ फिर भी ढूँढ सके। लापरवाही से लिखा DELETE ... LIKE '_smallpict%' उन markers को हटा देगा, और ऐसा होने पर टेस्ट फ़ेल हो जाता है।

हमने गति और सेटअप लागत के कारण वर्डप्रेस PHPUnit टेस्ट सूट की जगह सादी स्क्रिप्ट चुनीं: इन्हें न डेटाबेस सर्वर चाहिए, न वर्डप्रेस का checkout, और सभी 9 एक सेकंड से कम में चल जाती हैं, इसलिए सात PHP संस्करणों पर इन्हें चलाने से pull request में लगभग कोई समय नहीं जुड़ता। इसकी कीमत यह है कि stubs केवल वही दर्शाते हैं जो हमने उनमें लिखा है। जो व्यवहार असली वर्डप्रेस core पर निर्भर है, जैसे मीडिया लाइब्रेरी की अपनी metadata हैंडलिंग, वह इन टेस्ट से कवर नहीं होता।

WordPress.org पर डिप्लॉय से पहले का गेट

रिलीज़ एक अलग workflow से जाती हैं। v1.2.9 जैसा टैग push करने से यह शुरू होता है, और WordPress.org SVN में commit करने वाला job lint matrix पर निर्भरता घोषित करता है:

YAML
jobs:  lint:    name: WPCS Check (PHP ${{ matrix.php-version }})    runs-on: ubuntu-26.04    strategy:      matrix:        php-version: ['7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5']    steps:      - uses: actions/checkout@v7      - uses: shivammathur/setup-php@v2        with:          php-version: ${{ matrix.php-version }}      - run: composer install --prefer-dist --no-progress --no-interaction      - name: PHP CodeSniffer Check        run: vendor/bin/phpcs --standard=phpcs.xml.dist
  deploy:    name: Deploy    needs: lint    runs-on: ubuntu-26.04    permissions:      contents: read    steps:      - uses: actions/checkout@v7      - uses: shivammathur/setup-php@v2        with:          php-version: '7.4'      - name: Build WP.org Version        run: |          chmod +x build-wporg.sh          ./build-wporg.sh      - name: Deploy to WordPress.org        uses: 10up/action-wordpress-plugin-deploy@stable        with:          generate-zip: false        env:          SVN_USERNAME: ${{ secrets.SVN_USERNAME }}          SVN_PASSWORD: ${{ secrets.SVN_PASSWORD }}          SLUG: smallpict          BUILD_DIR: build-wporg/smallpict          ASSETS_DIR: .wordpress-org

needs: lint का मतलब है कि deploy job तभी शुरू होता है जब सातों lint jobs सफल हों। किसी भी संस्करण पर WPCS फ़ेल हो तो GitHub deploy को छोड़ देता है और कोई SVN commit नहीं होता। बिल्ड step PHP 7.4 पर चलता है, जो हमारा समर्थित सबसे पुराना संस्करण है। job के token को रिपॉजिटरी पर केवल read-only एक्सेस है; उसका एकमात्र write WordPress.org SVN पर होता है, जिसके credentials रिपॉजिटरी secrets में रखे हैं।

Deploy to WordPress.org GitHub Actions workflow में डिप्लॉय से पहले का गेट: Deploy job चलने से पहले PHP 7.4, 8.0, 8.1, 8.2 और 8.3 पर WPCS Check पास

चित्र 4: Deploy to WordPress.org workflow। पाँच WPCS Check jobs पहले चलते हैं; उनके नीचे वाला Deploy job तभी चलता है जब पाँचों पास हो जाएँ।

गेट पिछले pull request रन पर भरोसा करने के बजाय टैग किए गए commit पर WPCS फिर से चलाता है। टैग किसी भी commit की ओर इशारा कर सकता है, जिसमें सीधे push किया गया commit या ऐसा commit भी शामिल है जिसकी जाँचें कभी पूरी नहीं हुईं, और रिलीज़ workflow के पास यह जानने का कोई दूसरा तरीका नहीं कि वह क्या शिप कर रहा है।

CI जाँचें लोकल रूप से चलाना

Composer स्क्रिप्ट वही कमांड चलाती हैं जो CI चलाता है, इसलिए push करने से पहले किसी विफलता को दोहराया जा सकता है:

Shell
composer installcomposer lint        # vendor/bin/phpcs --standard=phpcs.xml.distcomposer lint-fix    # vendor/bin/phpcbf: fixes spacing and formatting sniffscomposer phpstan     # vendor/bin/phpstan analysefor t in tests/test_.php do php "$t"  break done

किसी एक matrix job की विफलता दोहराने के लिए वही कमांड उस PHP संस्करण पर चलाएँ, उदाहरण के लिए composer install के बाद आधिकारिक इमेज के साथ:

Shell
docker run --rm -v "$PWD":/app -w /app php:7.4-cli \  sh -c 'vendor/bin/phpstan analyse --memory-limit=2G && for t in tests/test_*.php; do php "$t" || exit 1; done'

phpcbf केवल वही ठीक करता है जो कोई sniff यांत्रिक रूप से ठीक कर सके। escaping, sanitizing और nonce से जुड़ी खोजों में किसी व्यक्ति को तय करना होता है कि संदर्भ में कौन-सा फ़ंक्शन सही बैठता है, और हम इन्हें ऑटो-फ़िक्स नहीं करते।

पाइपलाइन अभी क्या कवर नहीं करती

तीन कमियाँ हमारी सूची में हैं:

  1. डिप्लॉय से पहले का गेट केवल WPCS चलाता है। PHPStan और ऑटोमेटेड टेस्ट master और develop पर push और pull requests पर चलते हैं, रिलीज़ टैग पर नहीं। डिप्लॉय workflow के lint job में ये दोनों steps जोड़ने से यह कमी दूर होगी।
  2. हमारे ruleset में PHPCompatibility शामिल नहीं है। 7.4- के testVersion के साथ phpcompatibility/phpcompatibility-wp जोड़ने से एक ही रन में वह सिंटैक्स और वे फ़ंक्शन फ़्लैग हो जाएँगे जो 7.4 पर मौजूद नहीं हैं।
  3. टेस्ट असली वर्डप्रेस इंस्टॉल के बजाय stubs का उपयोग करते हैं। वर्डप्रेस 5.9 (हमारा न्यूनतम) और नवीनतम रिलीज़ पर एक integration job वह core व्यवहार कवर करेगा जो stubs नहीं कर सकते।

अक्सर पूछे जाने वाले प्रश्न

वर्डप्रेस प्लगइन के लिए PHP 7.4 से 8.5 तक matrix टेस्टिंग क्यों ज़रूरी है?

WordPress.org एक ही रिलीज़ हर साइट तक पहुँचाता है, और प्लगइन हेडर (Requires PHP: 7.4) ही एकमात्र फ़िल्टर है जो तय करता है कि किन PHP संस्करणों पर वह इंस्टॉल हो। 7.4 और 8.5 के बीच PHP ने यह बदल दिया कि internal फ़ंक्शन गलत आर्ग्युमेंट टाइप के साथ कैसा व्यवहार करते हैं, संख्याओं की strings से तुलना कैसे होती है, और कौन-से पैटर्न deprecation देते हैं, इसलिए जो कोड एक संस्करण पर सही है वह दूसरे पर fatal error दे सकता है या चेतावनियाँ लॉग कर सकता है। एक ही PHP संस्करण पर CI रन केवल यह साबित करता है कि प्लगइन उस संस्करण पर काम करता है। हर समर्थित संस्करण पर PHPStan और टेस्ट चलाने से ये अंतर रिलीज़ के उपयोगकर्ताओं तक पहुँचने से पहले पकड़ में आ जाते हैं।

क्या WPCS पारंपरिक सुरक्षा ऑडिट की जगह ले सकता है?

नहीं। WPCS पैटर्न को उसी पंक्ति पर जाँचता है जहाँ वे होते हैं: sanitize किए बिना पढ़ा गया रिक्वेस्ट वेरिएबल, escaping के बिना echo, nonce जाँच के बिना handler, $wpdb->prepare() के बिना क्वेरी। यह नहीं बता सकता कि handler सही capability जाँचता है या नहीं, escaping फ़ंक्शन आउटपुट के संदर्भ में सही बैठता है या नहीं, कोई server-side रिक्वेस्ट उपयोगकर्ता के नियंत्रण वाले URL पर जाती है या नहीं, या कोई उपयोगकर्ता दूसरे उपयोगकर्ता के डेटा तक पहुँच सकता है या नहीं। phpcs:ignore टिप्पणी किसी sniff को चुप भी करा सकती है। हम WPCS को न्यूनतम स्तर मानते हैं जो ज्ञात गलतियों को बाहर रखता है, और सुरक्षा से जुड़े बदलावों की समीक्षा हाथ से करते हैं।

PHPStan कोड चलाए बिना वर्डप्रेस core फ़ंक्शनों को कैसे संभालता है?

PHPStan जिस कोड का विश्लेषण करता है उसे कभी नहीं चलाता। वह टाइप का मॉडल बनाने के लिए declarations और docblocks पढ़ता है। वर्डप्रेस के लिए szepeviktor/phpstan-wordpress stub फ़ाइलें देता है: वर्डप्रेस core के फ़ंक्शनों और क्लासों के signatures, पैरामीटर टाइप और return टाइप, उनकी body के बिना। PHPStan इन stubs को ऐसे पढ़ता है मानो वे वर्डप्रेस हों, इसलिए वह जाँच सकता है कि wp_delete_file() या get_attached_file() का कॉल सही टाइप पास करता और प्राप्त करता है। वर्डप्रेस या प्लगइन रनटाइम पर जो constants परिभाषित करते हैं, वे bootstrap फ़ाइल में जाते हैं, जिसे PHPStan विश्लेषण से पहले लोड करता है।

प्लगइन WordPress.org पर smallPict नाम से उपलब्ध है। इंस्टॉलेशन और आउटपुट फ़ॉर्मेट सेटिंग्स इंस्टॉलेशन गाइड और इमेज सेटिंग्स संदर्भ में दी गई हैं।

smallPict Engineering द्वारा लिखित

Engineering

हम वेबसाइटों और ऐप्स के लिए इमेज रूपांतरण और CDN डिलीवरी बनाते हैं।

अपनी इमेज पर आज़माकर देखें।

हर महीने 20 MB मुफ़्त। WordPress पर सेटअप में लगभग दो मिनट।

इमेज परफ़ॉर्मेंस, Core Web Vitals और क्लाउड स्टोरेज पर और लेख।

सभी लेख

Web Performance5 min read

आधुनिक वर्डप्रेस साइटों को दोहरे AVIF/WebP की आवश्यकता क्यों है (और ब्राउज़रों को तोड़े बिना इसे कैसे प्रदान करें)

HTML5 <picture> टैग के माध्यम से WebP फ़ॉलबैक के साथ AVIF प्रदान करें, ताकि आपकी सबसे भारी छवियां छोटी हों और मोबाइल LCP में मदद मिले, पुराने ब्राउज़रों को तोड़े बिना और हर थंबनेल पर क्लाउड कोटा खर्च किए बिना।

Cloud & CDN3 min read

SmallPict API/SDK: BYOK बनाम प्रबंधित CDN, आपको कौन सा चुनना चाहिए?

Ignite में कोई CDN नहीं है (नतीजे सीधे आपके ऐप को लौटते हैं), जबकि Velocity और Momentum में आप SmallPict स्टोरेज और CDN (डिफ़ॉल्ट) या अपने स्टोरेज और CDN के बीच चुन सकते हैं। जानिए कैसे तय करें कि आपके प्रोजेक्ट के लिए कौन सा मॉडल सही है।

WordPress3 min read

CDN के साथ WordPress इमेज ऑप्टिमाइज़ेशन: क्या शामिल है और उचित उपयोग क्या है

SmallPict के सशुल्क WordPress प्लान में हाई-स्पीड लोकल डिलीवरी (Starter) या वैकल्पिक प्रबंधित वैश्विक CDN (Pro और Business) शामिल है। जानिए ठीक-ठीक क्या शामिल है, उचित उपयोग किसे माना जाता है, और कई साइटों में हमारा साझा उपयोग पूल कैसे काम करता है।