Zum Inhalt springen
smallPict
Kostenlos Starten

Engineering18 min read

WordPress-Plugin-CI/CD: WPCS, PHPStan und PHP 7.4 bis 8.5

WordPress-Plugin-CI/CD mit GitHub Actions: WPCS, PHPStan und automatisierte Tests auf PHP 7.4 bis 8.5, dazu ein Pre-Deploy-Gate vor WordPress.org.

smallPict Engineering

Engineering

Das WordPress-Plugin von smallPict konvertiert Uploads in WebP und AVIF und liefert sie an Besucher aus. Wenn wir ein Release taggen, committet GitHub Actions es in das SVN-Repository von WordPress.org, und von dort gelangt es auf jede Website, auf der das Plugin installiert ist. Websites mit aktivierten automatischen Updates installieren es, ohne dass jemand die Änderung prüft. Das Plugin deklariert Requires PHP: 7.4 und Requires at least: 5.9, also muss ein und derselbe Build auf einem Shared Host laufen, der noch PHP 7.4 nutzt, und ebenso in einem Container mit PHP 8.5.

Ein Fatal Error in einem Plugin kann wp-admin lahmlegen. Ein nicht escapter Wert auf einer Einstellungsseite ist eine XSS-Lücke auf allen Websites gleichzeitig. Eine fehlende Nonce-Prüfung macht eine Admin-Aktion zum CSRF-Ziel. Nichts davon lässt sich still beheben: Der Fix braucht ein weiteres Release, und manche Websites behalten die fehlerhafte Version, bis es eintrifft.

Dieser Beitrag beschreibt unsere CI/CD-Pipeline für das WordPress-Plugin: die WordPress Coding Standards (WPCS), PHPStan und die automatisierten Tests, die auf sieben PHP-Versionen laufen, sowie das Pre-Deploy-Gate vor WordPress.org. Alles Folgende stammt aus dem Repository smallpict-wp in Release 1.2.9.

Das Wichtigste in Kürze

  • Jeder Pull Request nach master führt WPCS, PHPStan und 9 automatisierte Testskripte auf PHP 7.4, 8.0, 8.1, 8.2, 8.3, 8.4 und 8.5 parallel in GitHub Actions aus.
  • WPCS erzwingt bereinigte Eingaben, escapte Ausgaben, Nonces und Prepared Queries. PHPStan auf Level 5 findet Typfehler und tote Zweige anhand von WordPress-Stubs, ohne WordPress auszuführen.
  • Ein Release-Tag führt WPCS erneut auf allen sieben PHP-Versionen aus, und der Job, der in das SVN von WordPress.org committet, startet erst, wenn alle sieben bestanden haben.

Die WordPress-Plugin-CI/CD-Pipeline im Überblick

Zwei GitHub-Actions-Workflows sichern das Plugin ab.

WorkflowAuslöserJobsPrüfungen
phpcs.yml (PHP Code Quality & Standards)Push nach master oder develop, Pull Requests nach masterein Job pro PHP-Version: 7.4, 8.0, 8.1, 8.2, 8.3, 8.4, 8.5WPCS, PHPStan, automatisierte Tests
deploy-all.yml (Deploy to WordPress.org)Push eines v*-TagsWPCS auf jeder der 7 PHP-Versionen, danach deployWPCS, danach Build und SVN-Commit

Hinweis zur Abdeckung der PHP-Versionen: Die Screenshots in diesem Beitrag zeigen unsere früheren CI-Läufe mit PHP 7.4 bis 8.3. Inzwischen deckt unsere Testmatrix in GitHub Actions PHP 7.4 bis PHP 8.5 ab. So bleibt das Plugin dauerhaft mit aktuellen PHP-Laufzeitumgebungen kompatibel, ohne Regressionen auf älteren Umgebungen.

Das gesamte Tooling ist als Composer-Dev-Dependency eingebunden, nichts davon wird an Nutzer ausgeliefert:

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"}

Multi-Version-PHP-Matrix-Tests in GitHub Actions

Was sich zwischen PHP 7.4 und PHP 8.5 ändert

PHP 7.4 und PHP 8.5 führen denselben Plugin-Quellcode unterschiedlich aus. Einige der Änderungen, die WordPress-Plugin-Code betreffen:

  • Seit PHP 8.0 werfen viele interne Funktionen bei einem Argument falschen Typs einen TypeError, wo 7.4 mit einer Warnung null zurückgab. Code, der ein Array an strlen() übergab, lief auf 7.4 weiter und bricht auf 8.0 ab.
  • PHP 8.0 hat geändert, wie eine Zahl mit einem nicht numerischen String verglichen wird. 0 == "abc" ist auf 7.4 true und auf 8.0 false, was manche losen Vergleiche auf Optionswerten unbemerkt umkehrt.
  • PHP 8.0 hat str_starts_with() und verwandte Funktionen eingeführt. Unser Bildprozessor ruft str_starts_with() auf. Auf PHP 7.4 funktioniert das nur, weil WordPress ab 5.9 ein Polyfill mitliefert, was unser Requires at least: 5.9 garantiert.
  • PHP 8.1 hat die Übergabe von null an nicht-nullable Parameter interner Funktionen als veraltet markiert. strtolower( wp_parse_url( $url, PHP_URL_HOST ) ) ist auf 7.4 unproblematisch und protokolliert auf 8.1 eine Deprecation, wenn $url relativ und der Host null ist. Manche Hoster geben Deprecations direkt in der Seite aus.
  • PHP 8.2 hat dynamische Properties als veraltet markiert. Eine Zuweisung an eine nicht deklarierte Property einer Ihrer eigenen Klassen protokolliert bei jedem Request eine Deprecation.

Jede dieser Änderungen zeigt sich erst, wenn der Code auf der PHP-Version analysiert oder ausgeführt wird, die sich anders verhält. Deshalb lässt die Pipeline jeden Pull Request nach master auf allen sieben Versionen gleichzeitig laufen.

Die GitHub-Actions-Matrix

CI-Lauf eines WordPress-Plugins in GitHub Actions: PHP-Matrix-Tests mit fünf parallelen WPCS- und PHPStan-Jobs auf PHP 7.4, 8.0, 8.1, 8.2 und 8.3, alle erfolgreich

Abbildung 1: Der Workflow PHP Code Quality & Standards für den Merge der 1.2.0-Arbeiten nach master. Jede PHP-Version ist ein eigener Job, und der Job für PHP 8.3 zeigt die Schritte, die jeder Job ausführt: PHPCS, PHPStan, danach die automatisierten Tests.

Hinweis zur Abdeckung der PHP-Versionen: Die Screenshots oben zeigen unsere früheren CI-Läufe mit PHP 7.4 bis 8.3. Inzwischen deckt unsere Testmatrix in GitHub Actions PHP 7.4 bis PHP 8.5 ab. So bleibt das Plugin dauerhaft mit aktuellen PHP-Laufzeitumgebungen kompatibel, ohne Regressionen auf älteren Umgebungen.

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 behält seinen Standardwert true: Schlägt eine Version fehl, bricht GitHub die noch laufenden Jobs ab. Ein fehlerhafter Pull Request kostet dadurch wenig, um den Preis, dass nicht das Ergebnis jeder Version im selben Lauf sichtbar ist.

Was die Matrix belegt und was nicht

Die drei Prüfungen profitieren nicht gleich stark von der Matrix. PHPStan analysiert den Code für die PHP-Version, auf der es läuft, und die Tests führen den Code auf dieser Version aus, sodass beide je Job ein anderes Ergebnis liefern können. PHPCS arbeitet auf Tokens und liefert in der Praxis auf allen sieben dasselbe Ergebnis; in der Matrix belegt es vor allem, dass sich die Toolchain auf jeder Version installieren und ausführen lässt. Syntax und Funktionen in einem Durchgang gegen einen Bereich von PHP-Versionen zu prüfen, ist Aufgabe von PHPCompatibility, das unser Ruleset noch nicht enthält.

WordPress Coding Standards (WPCS) automatisieren

Die Security-Sniffs, auf die es bei einem Plugin ankommt

WPCS ist eine Sammlung von PHP_CodeSniffer-Sniffs, die für WordPress geschrieben wurden. Die für die Plugin-Sicherheit wichtigsten Regeln stehen im WordPress-Ruleset selbst:

  • WordPress.Security.ValidatedSanitizedInput meldet jeden Lesezugriff auf $_POST, $_GET, $_REQUEST oder $_COOKIE, bei dem die Slashes nicht entfernt werden und der Wert nicht bereinigt wird.
  • WordPress.Security.EscapeOutput meldet jedes echo oder print eines Werts, der keine Escaping-Funktion wie esc_html(), esc_attr() oder esc_url() durchlaufen hat.
  • WordPress.Security.NonceVerification meldet einen Handler, der Request-Daten liest, ohne eine Nonce zu prüfen.
  • WordPress.DB.PreparedSQL und PreparedSQLPlaceholders melden eine Query, die per String-Verkettung statt mit $wpdb->prepare() gebaut wird, sowie Platzhalter, die nicht zu ihren Argumenten passen.

Unser WPCS-Ruleset

phpcs.xml.dist fügt WordPress-Extra und WordPress-Docs hinzu, legt die Text-Domain für die Übersetzungs-Sniffs fest und verlangt, dass jede globale Funktion, Klasse und Konstante unser Präfix trägt, damit das Plugin auf derselben Website nicht mit den Namen eines anderen Plugins kollidiert:

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>

Ein AJAX-Handler, der WPCS besteht

Dieser Handler aus dem Plugin stellt in den Cloud-Speicher ausgelagerte Medien stapelweise wieder her. Jede Eingabegrenze wird in seinen ersten Zeilen behandelt:

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 und Capability werden zusammen geprüft, denn eine gültige Nonce belegt, dass der Request von unserer Admin-Seite kam, nicht, dass der Benutzer die Aktion ausführen darf. $_POST['after'] wird von Slashes befreit und mit absint() umgewandelt, bevor es die Query erreicht, und die Query läuft trotzdem durch $wpdb->prepare(), obwohl der Wert bereits ein Integer ist.

Unterdrückungen brauchen einen Sniff-Namen und eine Begründung

Die phpcs:ignore-Zeile oben ist Absicht. WPCS warnt vor direkten Datenbankabfragen ohne Object Caching; für Lesezugriffe im Frontend ist das ein guter Rat, für eine Admin-Aktion, deren Zeilen sich bei jedem Aufruf ändern, der falsche. Unsere Regel: Jedes Ignore nennt den exakten Sniff und gibt die Begründung nach -- an. Ein nacktes // phpcs:ignore kommt nicht durchs Review.

In der CI schreibt PHPCS einen Checkstyle-Report, und cs2pr macht daraus Annotationen im Diff des Pull Requests, sodass ein Reviewer jeden Verstoß an der Zeile sieht, die ihn verursacht hat, statt in einem Log.

Logikfehler mit statischer Analyse durch PHPStan finden

WPCS liest Code Zeile für Zeile. Es erkennt, dass einem echo ein Escaping-Aufruf fehlt, aber nicht, welchen Typ eine Variable hat oder ob ein Zweig jemals ausgeführt werden kann. PHPStan baut ein Modell der Typen, die durch jede Funktion fließen, und meldet Code, der diesem Modell widerspricht: Aufrufe von Methoden, die nicht existieren, ein string|false, wo ein string verlangt ist, Vergleiche von Werten, die nie gleich sein können, Bedingungen, die immer wahr oder immer falsch sind.

PHPStan mit WordPress nutzen, ohne WordPress auszuführen

szepeviktor/phpstan-wordpress liefert PHPStan die Signaturen der Funktionen und Klassen des WordPress-Cores über Stub-Dateien, dazu einige WordPress-spezifische Typ-Erweiterungen. phpstan/extension-installer registriert es automatisch. PHPStan liest diese Deklarationen und lädt weder WordPress noch eine Datenbank.

Unsere PHPStan-Konfiguration

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

Level 5 prüft den Typ jedes Arguments, das an eine Funktion oder Methode übergeben wird, zusätzlich zu den Prüfungen der niedrigeren Level auf unbekannte Klassen, Methoden und Properties, Rückgabetypen und toten Code. treatPhpDocTypesAsCertain: false weist PHPStan an, unsere eigenen @param-Docblocks nicht als Beweis zu werten. WordPress reicht weiter, was ein Filter zurückgibt, sodass ein als int dokumentierter Parameter von einem anderen Plugin trotzdem als String ankommen kann, und PHPStan soll die defensiven Prüfungen, die diesen Fall abfangen, stehen lassen. Die Bootstrap-Datei definiert die Konstanten des Plugins (SMALLPICT_PATH, SMALLPICT_URL und weitere), die normalerweise zur Laufzeit gesetzt werden.

PHPStan-Log der statischen WordPress-Analyse in GitHub Actions: WPCS-Schritt ohne Befund, PHPStan hat 11 von 11 Plugin-Dateien analysiert, Ergebnis „[OK] No errors“

Abbildung 2: Die Schritte PHPCS und PHPStan des Jobs für PHP 8.3. PHPCS gibt nichts aus, wenn es nichts zu melden gibt. PHPStan analysiert die 11 Plugin-Dateien (die Hauptdatei des Plugins und die 10 Klassen in includes/) und meldet keine Fehler.

Ein echter PHPStan-Befund: Zeile 626 des Bildprozessors

Am 2. Oktober 2026 schlug die Pipeline bei PHPStan mit einem einzigen Fehler fehl:

PHPStan findet einen Bug in einem WordPress-Plugin: „Right side of && is always true“ in includes/class-smallpict-image-processor.php, Zeile 626, Regel booleanAnd.rightAlwaysTrue, Job auf PHP 8.2 fehlgeschlagen

Abbildung 3: Der Job für PHP 8.2 brach im PHPStan-Schritt mit booleanAnd.rightAlwaysTrue in Zeile 626 von includes/class-smallpict-image-processor.php ab. Zu diesem Zeitpunkt lagen 9 Dateien des Plugins in den analysierten Pfaden.

Der Code ist der letzte Schritt einer Konvertierung. Wenn die Plugin-Einstellung Ausgabeformat auf WebP oder AVIF steht, wird die konvertierte Datei zum Anhang, und der ursprüngliche Upload wird gelöscht, um Speicherplatz zu sparen. Die anderen Optionen, „AVIF, mit WebP als Ersatz“ und „Originalformat beibehalten“, behalten die Original-JPEG- und -PNG-Dateien. Das war der Lösch-Zweig:

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 meldete, dass file_exists( $main_file_path ) an dieser Stelle immer wahr ist. Der Grund steht 150 Zeilen weiter oben, am Anfang derselben Methode:

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.' );}

Die Methode läuft nur weiter, wenn die Datei existiert, und PHPStan trägt diese Tatsache bis Zeile 626 mit.

Wir haben den Befund nicht ungeprüft übernommen. file_exists() fragt das Dateisystem ab, und die Antwort kann sich zwischen zwei Aufrufen ändern. PHPStan merkt sich das Ergebnis eines solchen Aufrufs standardmäßig (Option rememberPossiblyImpureFunctionValues), unter der Annahme, dass dazwischen nichts die Festplatte verändert hat. Bevor wir die Prüfung entfernten, haben wir zwei Dinge durch Lesen des Codes bestätigt:

  1. Nichts zwischen dem Guard und Zeile 626 löscht oder verschiebt die Quelldatei. Die Konvertierung schreibt jede Ausgabe in eine temporäre Datei und verschiebt sie an einen neuen Pfad, und die einzigen wp_delete_file()-Aufrufe auf diesem Pfad entfernen temporäre Dateien. Der Offload-Schritt, der tatsächlich lokale Dateien löscht, läuft nach Zeile 626.
  2. wp_delete_file() kommt ohnehin mit einer fehlenden Datei zurecht. Die Funktion reicht den Pfad durch den Filter wp_delete_file und ruft @unlink() auf, was keinen Schaden anrichtet, wenn die Datei bereits weg ist.

Nachdem beides bestätigt war, war die Bedingung toter Code, und toter Code in einem Lösch-Zweig verleitet den nächsten Leser zu der Annahme, die Datei könne mitten in der Methode verschwinden. Der Fix hat sie entfernt:

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 ); }

Der Befund war kein Absturz. Sein Wert lag in dem, was er über die Methode aussagte: Die Annahmen des Codes hatten sich von seiner Struktur entfernt, und zwar in dem einen Zweig, der Benutzerdateien löscht. Die Lehre, die wir daraus gezogen haben, ist enger als „vertraue dem Analyzer“. Eine Meldung „immer wahr“ oder „immer falsch“ zu einer Dateisystem-, Netzwerk- oder Zeitfunktion verdient eine manuelle Prüfung dessen, was zwischen den beiden Aufrufen passiert, bevor die Bedingung entfernt wird.

Automatisierte Tests für ein WordPress-Plugin ohne PHPUnit

Der letzte Schritt jedes Matrix-Jobs führt die Dateien in tests/ aus. Das sind keine PHPUnit-Tests. Jede Datei ist ein einfaches PHP-Skript, das minimale Stubs für die benötigten WordPress-Funktionen definiert (get_option(), update_option(), sanitize_key() usw.), die zu testende Plugin-Klasse lädt, sie ausführt und beim ersten Fehler mit einem Exit-Code ungleich null endet:

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

Derzeit gibt es 9 solcher Skripte. Sie decken die Pfade ab, in denen ein Fehler einen Kunden Daten oder Geld kostet: die Konvertierungspipeline für WebP und AVIF samt ihren Fallbacks, die Bildauslieferung, das Auslagern und Löschen von Dateien im Cloud-Speicher, die stapelweise Wiederherstellung ausgelagerter Dateien, das Verhalten, wenn ein Konto nicht mehr zahlt, das Speicherkontingent und die Deinstallation. Der Deinstallationstest führt die echten Deinstallations-Queries gegen eine In-Memory-SQLite-Datenbank aus und prüft, was übrig bleibt:

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 ) . ')');

Diese Assertion schützt eine Entscheidung, die sich leicht versehentlich rückgängig machen lässt. Die Deinstallation des Plugins entfernt seine Einstellungen, behält aber die Post-Meta, die festhalten, welche Dateien nur im Cloud-Speicher liegen, damit eine Website, die das Plugin neu installiert, ihre Bilder weiterhin findet. Ein unbedachtes DELETE ... LIKE '_smallpict%' würde diese Markierungen entfernen, und in dem Fall schlägt der Test fehl.

Wir haben uns wegen Geschwindigkeit und Einrichtungsaufwand für einfache Skripte statt der PHPUnit-Testsuite von WordPress entschieden: Sie brauchen weder einen Datenbankserver noch einen WordPress-Checkout, und alle 9 laufen in weniger als einer Sekunde, sodass ihre Ausführung auf sieben PHP-Versionen einen Pull Request kaum verlängert. Der Kompromiss: Stubs bilden nur ab, was wir hineingeschrieben haben. Verhalten, das vom echten WordPress-Core abhängt, etwa die eigene Metadatenverarbeitung der Mediathek, decken diese Tests nicht ab.

Das Pre-Deploy-Gate vor WordPress.org

Releases laufen über einen separaten Workflow. Der Push eines Tags wie v1.2.9 startet ihn, und der Job, der in das SVN von WordPress.org committet, deklariert eine Abhängigkeit von der 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 bedeutet, dass der Job deploy erst startet, wenn alle sieben lint-Jobs erfolgreich waren. Schlägt WPCS auf irgendeiner Version fehl, überspringt GitHub deploy, und es findet kein SVN-Commit statt. Der Build-Schritt läuft auf PHP 7.4, der ältesten Version, die wir unterstützen. Das Token des Jobs hat nur Lesezugriff auf das Repository; sein einziger Schreibzugriff geht an das SVN von WordPress.org, mit Zugangsdaten, die als Repository-Secrets hinterlegt sind.

Pre-Deploy-Gate im GitHub-Actions-Workflow Deploy to WordPress.org: WPCS Check auf PHP 7.4, 8.0, 8.1, 8.2 und 8.3 bestanden, bevor der Deploy-Job lief

Abbildung 4: Der Workflow Deploy to WordPress.org. Die fünf Jobs WPCS Check laufen zuerst; der Job Deploy darunter läuft erst, wenn alle fünf bestanden haben.

Das Gate führt WPCS auf dem getaggten Commit erneut aus, statt sich auf den früheren Lauf des Pull Requests zu verlassen. Ein Tag kann auf jeden beliebigen Commit zeigen, auch auf einen direkt gepushten oder einen, dessen Prüfungen nie abgeschlossen wurden, und der Release-Workflow hat keinen anderen Weg festzustellen, was er ausliefert.

Die CI-Prüfungen lokal ausführen

Die Composer-Skripte kapseln dieselben Befehle, die die CI ausführt, sodass sich ein Fehler vor dem Push reproduzieren lässt:

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

Um einen Fehler aus einem einzelnen Matrix-Job zu reproduzieren, führen Sie dieselben Befehle unter der betreffenden PHP-Version aus, zum Beispiel mit dem offiziellen Image nach 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 behebt nur, was ein Sniff mechanisch beheben kann. Bei Befunden zu Escaping, Sanitizing und Nonces muss ein Mensch entscheiden, welche Funktion zum Kontext passt, und wir beheben sie nicht automatisch.

Was die Pipeline noch nicht abdeckt

Drei Lücken stehen auf unserer Liste:

  1. Das Pre-Deploy-Gate führt nur WPCS aus. PHPStan und die automatisierten Tests laufen bei Pushes nach master und develop sowie bei Pull Requests, nicht auf dem Release-Tag. Beide Schritte in den lint-Job des Deploy-Workflows aufzunehmen, schließt diese Lücke.
  2. Unser Ruleset enthält PHPCompatibility nicht. phpcompatibility/phpcompatibility-wp mit einer testVersion von 7.4- würde Syntax und Funktionen, die es auf 7.4 nicht gibt, in einem einzigen Lauf melden.
  3. Die Tests nutzen Stubs statt einer echten WordPress-Installation. Ein Integrations-Job gegen WordPress 5.9 (unser Minimum) und das neueste Release würde das Core-Verhalten abdecken, das die Stubs nicht abbilden können.

FAQ

Warum sind Matrix-Tests von PHP 7.4 bis 8.5 für WordPress-Plugins so wichtig?

WordPress.org liefert dasselbe Release an jede Website aus, und der Plugin-Header (Requires PHP: 7.4) ist der einzige Filter dafür, auf welchen PHP-Versionen es installiert wird. Zwischen 7.4 und 8.5 hat PHP geändert, wie interne Funktionen mit falschen Argumenttypen umgehen, wie Zahlen mit Strings verglichen werden und welche Muster Deprecations auslösen; Code, der auf einer Version korrekt ist, kann daher auf einer anderen mit einem Fatal Error abbrechen oder Warnungen protokollieren. Ein CI-Lauf auf einer einzigen PHP-Version belegt nur, dass das Plugin auf dieser Version funktioniert. Wer PHPStan und die Tests auf jeder unterstützten Version ausführt, fängt diese Unterschiede ab, bevor das Release die Nutzer erreicht.

Ersetzt WPCS klassische Sicherheitsaudits?

Nein. WPCS prüft Muster an der Zeile, in der sie auftreten: eine Request-Variable, die ohne Sanitizing gelesen wird, ein echo ohne Escaping, ein Handler ohne Nonce-Prüfung, eine Query ohne $wpdb->prepare(). Es kann nicht beurteilen, ob ein Handler die richtige Capability prüft, ob die Escaping-Funktion zum Ausgabekontext passt, ob ein serverseitiger Request an eine URL geht, die ein Benutzer kontrolliert, oder ob ein Benutzer an die Daten eines anderen gelangen kann. Ein phpcs:ignore-Kommentar kann einen Sniff zudem stummschalten. Wir behandeln WPCS als Untergrenze, die bekannte Fehler fernhält, und prüfen sicherheitsrelevante Änderungen von Hand.

Wie geht PHPStan mit WordPress-Core-Funktionen um, ohne Code auszuführen?

PHPStan führt den Code, den es analysiert, nie aus. Es liest Deklarationen und Docblocks, um ein Modell der Typen aufzubauen. Für WordPress liefert szepeviktor/phpstan-wordpress Stub-Dateien: die Signaturen, Parametertypen und Rückgabetypen der Funktionen und Klassen des WordPress-Cores, ohne deren Rumpf. PHPStan liest diese Stubs, als wären sie WordPress, und kann so prüfen, dass ein Aufruf von wp_delete_file() oder get_attached_file() die richtigen Typen übergibt und zurückbekommt. Konstanten, die WordPress oder das Plugin zur Laufzeit definieren, kommen in eine Bootstrap-Datei, die PHPStan vor der Analyse lädt.

Das Plugin ist auf WordPress.org als smallPict verfügbar. Die Installation und die Einstellungen für das Ausgabeformat beschreiben die Installationsanleitung und die Referenz der Bildeinstellungen.

Von smallPict Engineering

Engineering

Wir entwickeln Bildkonvertierung und CDN-Auslieferung für Websites und Apps.

Testen Sie es mit Ihren eigenen Bildern.

20 MB pro Monat kostenlos. Etwa zwei Minuten Einrichtung bei WordPress.

Mehr zu Bildperformance, Core Web Vitals und Cloud-Speicher.

Alle Artikel

WordPress2 min read

WordPress-Bildoptimierung mit CDN: Was enthalten ist und was als Fair Use gilt

Die kostenpflichtigen WordPress-Tarife von SmallPict enthalten schnelle lokale Auslieferung (Starter) oder ein optionales verwaltetes globales CDN (Pro und Business). Hier erfahren Sie genau, was enthalten ist, was als Fair Use gilt und wie unser gemeinsamer Nutzungspool über mehrere Websites funktioniert.