{"id":20854,"date":"2026-10-07T09:14:00","date_gmt":"2026-10-07T07:14:00","guid":{"rendered":"https:\/\/www.lukaswojcik.com\/blog\/?p=20854"},"modified":"2026-10-05T10:21:53","modified_gmt":"2026-10-05T08:21:53","slug":"plugin-check-ci","status":"publish","type":"post","link":"https:\/\/www.lukaswojcik.com\/blog\/en\/wordpress-plugins-tricks\/tutorials-en-wordpress-plugins-tricks\/plugin-check-ci\/","title":{"rendered":"Plugin Check locally and in CI: findings before the review"},"content":{"rendered":"<p>Every plugin submitted to the WordPress.org directory goes through an automated scan before a person from the Plugins Team reads the code. Several of the points that reviewers ask about can be detected mechanically: a missing license header, output that is not escaped, a script loaded from a public CDN, a text domain that does not match the slug. Plugin Check, short PCP, is the tool published by WordPress.org for this job. According to its description it runs most of the checks used for new submissions, on any WordPress installation and before anything is uploaded.<\/p>\n<p>This article shows Plugin Check 2.1.0 in three places: in the admin screen, on the command line with WP-CLI and in a GitHub Actions workflow built on the official action. A small plugin with deliberate mistakes serves as the test object. For each mistake the article names the check and the result code that a run of Plugin Check 2.1.0 on WordPress 7.1.2 reports, and then shows the corrected version.<\/p>\n<h2>What Plugin Check examines<\/h2>\n<p>The current version in the directory is 2.1.0, released on 16 August 2026. It requires WordPress 6.3 and PHP 7.4; the plugin page lists \u201cTested up to\u201d 7.0.6, while the current WordPress release is 7.1.2. Version 2.1.0 added, among other things, a check for production-time changes to PHP error reporting.<\/p>\n<p>In the source of 2.1.0 the class <code>Default_Check_Repository<\/code> registers 34 checks. Each check has a slug such as <code>late_escaping<\/code> and belongs to one or more categories. Two kinds of checks exist. Static checks read the code without running it, mostly through PHP_CodeSniffer sniffs from the WordPress Coding Standards or from Plugin Check&#8217;s own sniff set, partly through custom logic. Runtime checks run the plugin, which has to be active, against a separate set of database tables and observe what it does; in 2.1.0 these are the five performance checks for script and style scope, script and style size, and loading strategy.<\/p>\n<table>\n<thead>\n<tr>\n<th>Category (slug)<\/th>\n<th>Examples of checks in 2.1.0<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>General (<code>general<\/code>)<\/td>\n<td><code>i18n_usage<\/code>, <code>php_error_reporting<\/code>, <code>ai_provider<\/code><\/td>\n<\/tr>\n<tr>\n<td>Plugin Repo (<code>plugin_repo<\/code>)<\/td>\n<td><code>plugin_readme<\/code>, <code>plugin_header_fields<\/code>, <code>file_type<\/code>, <code>offloading_files<\/code>, <code>setting_sanitization<\/code>, <code>prefixing<\/code>, <code>plugin_review_phpcs<\/code>, <code>trademarks<\/code><\/td>\n<\/tr>\n<tr>\n<td>Security (<code>security<\/code>)<\/td>\n<td><code>late_escaping<\/code>, <code>direct_file_access<\/code>, <code>safe_redirect<\/code>, <code>direct_db_queries<\/code>, <code>direct_db<\/code><\/td>\n<\/tr>\n<tr>\n<td>Performance (<code>performance<\/code>)<\/td>\n<td><code>enqueued_scripts_in_footer<\/code>, <code>performant_wp_query_params<\/code>, <code>enqueued_scripts_scope<\/code>, <code>non_blocking_scripts<\/code><\/td>\n<\/tr>\n<tr>\n<td>Accessibility (<code>accessibility<\/code>)<\/td>\n<td>none: the category is defined, but no built-in check of 2.1.0 is assigned to it<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>All security checks registered in 2.1.0 also carry the <code>plugin_repo<\/code> category, so a run limited to Plugin Repo already covers escaping and direct file access. That matters because the plugin&#8217;s FAQ states that a plugin typically has to pass all checks in the Plugin Repo category to be approved for the directory. The other categories are described as additional.<\/p>\n<p>Every result is either an <code>ERROR<\/code> or a <code>WARNING<\/code> and carries a severity; the default is 5, a missing license in the plugin header is reported with 9. Errors mark violations of directory requirements. Warnings point to code that is often, but not always, a problem: reading <code>$_GET<\/code> without a Nonce is fine for a harmless display parameter and a real hole in a form handler. The checks decide the type themselves, and the PHPCS ruleset of Plugin Check deliberately downgrades some sniffs, for example Nonce verification and input sanitization, to warnings.<\/p>\n<h2>Running it locally: admin screen and WP-CLI<\/h2>\n<p>Plugin Check is installed like any other plugin. The project README advises against running it on a production site, because runtime checks execute the plugin under test. After activation, the screen under Tools \u2192 Plugin Check is available to users who can manage plugins. It offers a plugin selector plus checkboxes for categories and for result types. Results are grouped by file with line, column, type, code and message, and since version 1.8.0 they can be exported as CSV, JSON or Markdown.<\/p>\n<p>For repeated runs the WP-CLI command is more practical. The argument is a plugin slug, a path or the URL of a ZIP file. By default the command only executes static checks; runtime checks need the workaround documented in the README, where <code>--require<\/code> loads the file <code>cli.php<\/code> of Plugin Check before WordPress starts. In addition, the plugin under test has to be active; for an inactive plugin Plugin Check skips the runtime checks without a notice, and <code>--checks=enqueued_scripts_scope<\/code> then ends with \u201cdoes not exist\u201d.<\/p>\n<pre><code># Install and activate Plugin Check (2.1.0 at the time of writing).\nwp plugin install plugin-check --activate\n\n# All static checks for wp-content\/plugins\/lw-hello-banner.\nwp plugin check lw-hello-banner\n\n# Include runtime checks (plugin must be active): load cli.php before WordPress boots.\nwp plugin check lw-hello-banner --require=.\/wp-content\/plugins\/plugin-check\/cli.php\n\n# Limit the run to categories or to single checks.\nwp plugin check lw-hello-banner --categories=plugin_repo,security\nwp plugin check lw-hello-banner --checks=late_escaping,i18n_usage\n\n# Errors only, as one JSON array for scripts.\nwp plugin check lw-hello-banner --ignore-warnings --format=strict-json\n\n# Show result codes, then ignore one specific code.\nwp plugin check lw-hello-banner --format=csv --fields=code,message\nwp plugin check lw-hello-banner --ignore-codes=textdomain_mismatch\n\n# Checks for an update of an existing directory plugin.\nwp plugin check lw-hello-banner --mode=update\n\n# What is available in the installed version.\nwp plugin list-checks --format=csv\nwp plugin list-check-categories<\/code><\/pre>\n<p>The most important flags of <code>wp plugin check<\/code> in version 2.1.0:<\/p>\n<table>\n<thead>\n<tr>\n<th>Flag<\/th>\n<th>Effect<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><code>--checks=<\/code>, <code>--exclude-checks=<\/code><\/td>\n<td>run only the listed check slugs, or skip them<\/td>\n<\/tr>\n<tr>\n<td><code>--categories=<\/code><\/td>\n<td>limit the run to categories, comma-separated<\/td>\n<\/tr>\n<tr>\n<td><code>--ignore-codes=<\/code><\/td>\n<td>hide individual result codes such as <code>textdomain_mismatch<\/code><\/td>\n<\/tr>\n<tr>\n<td><code>--ignore-warnings<\/code>, <code>--ignore-errors<\/code><\/td>\n<td>hide one of the two result types<\/td>\n<\/tr>\n<tr>\n<td><code>--format=<\/code><\/td>\n<td><code>table<\/code> (default), <code>csv<\/code>, <code>json<\/code>, <code>ctrf<\/code> and the variants <code>strict-table<\/code>, <code>strict-csv<\/code>, <code>strict-json<\/code>, <code>strict-ctrf<\/code><\/td>\n<\/tr>\n<tr>\n<td><code>--fields=<\/code><\/td>\n<td>choose columns, for example <code>code,message<\/code><\/td>\n<\/tr>\n<tr>\n<td><code>--exclude-directories=<\/code>, <code>--exclude-files=<\/code><\/td>\n<td>skip paths in file-based scans; <code>.git<\/code>, <code>vendor<\/code>, <code>vendor_prefixed<\/code>, <code>vendor-prefixed<\/code> and <code>node_modules<\/code> are excluded by default<\/td>\n<\/tr>\n<tr>\n<td><code>--severity=<\/code>, <code>--error-severity=<\/code>, <code>--warning-severity=<\/code><\/td>\n<td>show only results at or above a severity<\/td>\n<\/tr>\n<tr>\n<td><code>--mode=<\/code><\/td>\n<td><code>new<\/code> (default) or <code>update<\/code>; in update mode an outdated \u201cTested up to\u201d is reported as a warning instead of an error<\/td>\n<\/tr>\n<tr>\n<td><code>--slug=<\/code><\/td>\n<td>override the slug used for text domain and readme comparisons<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Check slugs and result codes are two different things. <code>--checks=i18n_usage<\/code> selects a check, <code>--ignore-codes=WordPress.WP.I18n.TextDomainMismatch<\/code> hides a single finding from it. The CLI documentation recommends <code>--format=csv --fields=code,message<\/code> to look up codes. One detail is relevant for scripts: in the 2.1.0 source the command prints its findings without setting an error exit status because of them. A shell script that is meant to fail therefore has to evaluate the output itself; in a test run against the faulty sample the command reported twelve errors and still ended with exit status 0. The GitHub Action does exactly that evaluation. With <code>--format=json<\/code> each file gets a line <code>FILE: \u2026<\/code> followed by its own JSON array, so the output as a whole is not valid JSON; <code>--format=strict-json<\/code> returns a single array, but without file names. Without findings, both formats print only a success line.<\/p>\n<h2>A sample plugin with built-in mistakes<\/h2>\n<p>The sample is a single file in the folder <code>wp-content\/plugins\/lw-hello-banner\/<\/code>, so the slug is <code>lw-hello-banner<\/code>. It prints a greeting at the top of the page, reads an optional name from the URL, offers a note field on Settings \u2192 General and loads a decorative script. It contains nine deliberate mistakes: no <code>readme.txt<\/code>, no license header, a text domain that differs from the slug, no guard against direct file access, a call to <code>error_reporting()<\/code>, <code>register_setting()<\/code> without sanitization, output without escaping, a translatable string with a placeholder but without a translator comment, and a script loaded from a CDN without version and loading arguments.<\/p>\n<pre><code>&lt;?php\n\/**\n * Plugin Name: LW Hello Banner\n * Description: Shows a greeting banner at the top of the front page.\n * Version:     0.1.0\n * Author:      Lukas Wojcik\n * Text Domain: hello-banner\n *\/\n\n\/\/ Intentionally faulty example for Plugin Check. Do not use on a live site.\n\nerror_reporting( E_ALL );\n\nfunction lw_hello_banner_register_setting() {\n\tregister_setting( 'general', 'lw_hello_banner_note' );\n\n\tadd_settings_field(\n\t\t'lw_hello_banner_note',\n\t\t__( 'Banner note', 'hello-banner' ),\n\t\t'lw_hello_banner_field',\n\t\t'general'\n\t);\n}\nadd_action( 'admin_init', 'lw_hello_banner_register_setting' );\n\nfunction lw_hello_banner_field() {\n\techo '&lt;input type=\"text\" name=\"lw_hello_banner_note\" value=\"' . get_option( 'lw_hello_banner_note' ) . '\"&gt;';\n}\n\nfunction lw_hello_banner_assets() {\n\twp_enqueue_script( 'lw-hello-banner', 'https:\/\/cdn.jsdelivr.net\/npm\/canvas-confetti@1.9.3\/dist\/confetti.browser.min.js' );\n}\nadd_action( 'wp_enqueue_scripts', 'lw_hello_banner_assets' );\n\nfunction lw_hello_banner_render() {\n\t$name = isset( $_GET['lw_name'] ) ? $_GET['lw_name'] : 'Guest';\n\t$note = get_option( 'lw_hello_banner_note', '' );\n\n\techo '&lt;div class=\"lw-hello-banner\"&gt;&lt;p&gt;' . sprintf( __( 'Hello, %s!', 'hello-banner' ), $name ) . ' ' . $note . '&lt;\/p&gt;&lt;\/div&gt;';\n}\nadd_action( 'wp_body_open', 'lw_hello_banner_render' );<\/code><\/pre>\n<h2>What Plugin Check reports for it<\/h2>\n<p>The following table lists the findings of <code>wp plugin check lw-hello-banner<\/code> with Plugin Check 2.1.0 on WordPress 7.1.2, twelve errors and nine warnings in total; a run with <code>--require<\/code> returns the same list. Line numbers are left out; <code>OutputNotEscaped<\/code> appears four times, <code>TextDomainMismatch<\/code> and <code>NonceVerification.Recommended<\/code> twice each.<\/p>\n<table>\n<thead>\n<tr>\n<th>Result code<\/th>\n<th>Check (category)<\/th>\n<th>Type<\/th>\n<th>Cause in the sample<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><code>no_plugin_readme<\/code><\/td>\n<td><code>plugin_readme<\/code> (plugin_repo)<\/td>\n<td>ERROR<\/td>\n<td>no <code>readme.txt<\/code><\/td>\n<\/tr>\n<tr>\n<td><code>plugin_header_no_license<\/code><\/td>\n<td><code>plugin_header_fields<\/code> (plugin_repo)<\/td>\n<td>ERROR<\/td>\n<td>no <code>License<\/code> header<\/td>\n<\/tr>\n<tr>\n<td><code>textdomain_mismatch<\/code><\/td>\n<td><code>plugin_header_fields<\/code> (plugin_repo)<\/td>\n<td>WARNING<\/td>\n<td>header says <code>hello-banner<\/code>, slug is <code>lw-hello-banner<\/code><\/td>\n<\/tr>\n<tr>\n<td><code>missing_direct_file_access_protection<\/code><\/td>\n<td><code>direct_file_access<\/code> (security, plugin_repo)<\/td>\n<td>ERROR<\/td>\n<td>functions and hooks without an <code>ABSPATH<\/code> guard<\/td>\n<\/tr>\n<tr>\n<td><code>WordPress.WP.I18n.TextDomainMismatch<\/code><\/td>\n<td><code>i18n_usage<\/code> (general, plugin_repo)<\/td>\n<td>ERROR<\/td>\n<td><code>__()<\/code> calls with the wrong text domain<\/td>\n<\/tr>\n<tr>\n<td><code>WordPress.WP.I18n.MissingTranslatorsComment<\/code><\/td>\n<td><code>i18n_usage<\/code> (general, plugin_repo)<\/td>\n<td>ERROR<\/td>\n<td><code>%s<\/code> without a <code>translators:<\/code> comment<\/td>\n<\/tr>\n<tr>\n<td><code>PluginCheck.CodeAnalysis.PHPErrorReporting.DirectErrorReportingCall<\/code><\/td>\n<td><code>php_error_reporting<\/code> (general)<\/td>\n<td>WARNING<\/td>\n<td><code>error_reporting( E_ALL )<\/code><\/td>\n<\/tr>\n<tr>\n<td><code>WordPress.PHP.DevelopmentFunctions.prevent_path_disclosure_error_reporting<\/code><\/td>\n<td><code>plugin_review_phpcs<\/code> (plugin_repo)<\/td>\n<td>WARNING<\/td>\n<td><code>error_reporting( E_ALL )<\/code><\/td>\n<\/tr>\n<tr>\n<td><code>PluginCheck.CodeAnalysis.SettingSanitization.register_settingMissing<\/code><\/td>\n<td><code>setting_sanitization<\/code> (plugin_repo)<\/td>\n<td>ERROR<\/td>\n<td><code>register_setting()<\/code> without a third argument<\/td>\n<\/tr>\n<tr>\n<td><code>WordPress.Security.EscapeOutput.OutputNotEscaped<\/code><\/td>\n<td><code>late_escaping<\/code> (security, plugin_repo)<\/td>\n<td>ERROR<\/td>\n<td><code>get_option()<\/code>, <code>__()<\/code>, <code>$name<\/code> and <code>$note<\/code> in <code>echo<\/code><\/td>\n<\/tr>\n<tr>\n<td><code>PluginCheck.CodeAnalysis.EnqueuedResourceOffloading.OffloadedContent<\/code><\/td>\n<td><code>offloading_files<\/code> (plugin_repo)<\/td>\n<td>ERROR<\/td>\n<td>script URL on <code>cdn.jsdelivr.net<\/code><\/td>\n<\/tr>\n<tr>\n<td><code>WordPress.WP.EnqueuedResourceParameters.MissingVersion<\/code>, <code>.NotInFooter<\/code><\/td>\n<td><code>enqueued_scripts_in_footer<\/code> (performance)<\/td>\n<td>WARNING<\/td>\n<td>no version, no fifth argument<\/td>\n<\/tr>\n<tr>\n<td><code>WordPress.Security.NonceVerification.Recommended<\/code><\/td>\n<td><code>plugin_review_phpcs<\/code> (plugin_repo)<\/td>\n<td>WARNING<\/td>\n<td><code>$_GET['lw_name']<\/code> without a Nonce check<\/td>\n<\/tr>\n<tr>\n<td><code>WordPress.Security.ValidatedSanitizedInput.MissingUnslash<\/code>, <code>.InputNotSanitized<\/code><\/td>\n<td><code>plugin_review_phpcs<\/code> (plugin_repo)<\/td>\n<td>WARNING<\/td>\n<td><code>$_GET['lw_name']<\/code> used raw<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<figure class=\"lw-diagram\">\n<img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/diagrams\/wp-plugin-check-en.png?v=20260930\" width=\"1120\" height=\"580\" alt=\"From the faulty sample plugin v0.1.0 to a clean run: Plugin Check 2.1.0 sorts its checks into General, Plugin Repo, Security, Performance and Accessibility; errors such as no_plugin_readme fail the CI job, warnings such as textdomain_mismatch are only annotated, and fixing leads to the corrected v0.2.0\"><figcaption>Plugin Check 2.1.0 sorts its checks into categories; errors fail the CI job, warnings are annotated<\/figcaption><\/figure>\n<p>Three observations from the table. First, the text domain is reported twice by different checks: the header check compares the <code>Text Domain<\/code> header with the slug and issues a warning, while the i18n sniff compares every translation call with the slug and issues an error. Second, the Nonce and sanitization findings do not come from the Security category but from <code>plugin_review_phpcs<\/code>, a PHPCS run with Plugin Check&#8217;s review ruleset; a run with <code>--categories=security<\/code> would not show them. Third, the ruleset also contains the sniff group <code>WordPress.PHP.DevelopmentFunctions<\/code> as a warning. It does report the <code>error_reporting()<\/code> line a second time, as <code>prevent_path_disclosure_error_reporting<\/code>.<\/p>\n<p>The CDN script leads to a further point: the runtime checks for script scope and loading strategy only evaluate scripts whose URL lies inside the plugin folder. The CDN script is therefore reported only by the static checks, and the run with <code>--require<\/code> returns the same list as the one without. The runtime checks only matter for the corrected version with its bundled script: without the <code>is_front_page()<\/code> condition it is reported as <code>EnqueuedScriptsScope<\/code> (\u201cThis script is being loaded in all frontend contexts.\u201d), but only in a run with <code>--require<\/code>.<\/p>\n<h2>The corrected version<\/h2>\n<p>The corrected plugin consists of three files: the main file, a <code>readme.txt<\/code> and a small local script in <code>assets\/<\/code>. The main file gains a complete header with license, <code>Requires at least<\/code> and the text domain <code>lw-hello-banner<\/code>, and an <code>ABSPATH<\/code> guard. The call to <code>error_reporting()<\/code> is gone. <code>register_setting()<\/code> receives type, <code>sanitize_callback<\/code> and default value. Output is escaped at the point of output with <code>esc_attr()<\/code>, <code>esc_html()<\/code> and <code>esc_html__()<\/code>. The script is bundled with the plugin, versioned and loaded only on the front page with <code>in_footer<\/code> and <code>strategy<\/code>; the array form of the fifth argument of <code>wp_enqueue_script()<\/code> exists since WordPress 6.3, which matches the declared minimum version.<\/p>\n<pre><code>&lt;?php\n\/**\n * Plugin Name:       LW Hello Banner\n * Description:       Shows a greeting banner at the top of the front page.\n * Version:           0.2.0\n * Requires at least: 6.3\n * Requires PHP:      7.4\n * Author:            Lukas Wojcik\n * License:           GPLv2 or later\n * License URI:       https:\/\/www.gnu.org\/licenses\/gpl-2.0.html\n * Text Domain:       lw-hello-banner\n *\/\n\nif ( ! defined( 'ABSPATH' ) ) {\n\texit;\n}\n\ndefine( 'LW_HELLO_BANNER_VERSION', '0.2.0' );\n\n\/**\n * Registers the banner note setting and its field on Settings &gt; General.\n *\/\nfunction lw_hello_banner_register_setting() {\n\tregister_setting(\n\t\t'general',\n\t\t'lw_hello_banner_note',\n\t\tarray(\n\t\t\t'type'              =&gt; 'string',\n\t\t\t'sanitize_callback' =&gt; 'sanitize_text_field',\n\t\t\t'default'           =&gt; '',\n\t\t)\n\t);\n\n\tadd_settings_field(\n\t\t'lw_hello_banner_note',\n\t\t__( 'Banner note', 'lw-hello-banner' ),\n\t\t'lw_hello_banner_field',\n\t\t'general',\n\t\t'default',\n\t\tarray( 'label_for' =&gt; 'lw_hello_banner_note' )\n\t);\n}\nadd_action( 'admin_init', 'lw_hello_banner_register_setting' );\n\n\/**\n * Prints the input field for the banner note.\n *\/\nfunction lw_hello_banner_field() {\n\tprintf(\n\t\t'&lt;input type=\"text\" class=\"regular-text\" id=\"lw_hello_banner_note\" name=\"lw_hello_banner_note\" value=\"%s\"&gt;',\n\t\tesc_attr( get_option( 'lw_hello_banner_note', '' ) )\n\t);\n}\n\n\/**\n * Loads the bundled script on the front page only.\n *\/\nfunction lw_hello_banner_assets() {\n\tif ( ! is_front_page() ) {\n\t\treturn;\n\t}\n\n\twp_enqueue_script(\n\t\t'lw-hello-banner',\n\t\tplugins_url( 'assets\/lw-hello-banner.js', __FILE__ ),\n\t\tarray(),\n\t\tLW_HELLO_BANNER_VERSION,\n\t\tarray(\n\t\t\t'in_footer' =&gt; true,\n\t\t\t'strategy'  =&gt; 'defer',\n\t\t)\n\t);\n}\nadd_action( 'wp_enqueue_scripts', 'lw_hello_banner_assets' );\n\n\/**\n * Prints the banner on the front page.\n *\/\nfunction lw_hello_banner_render() {\n\tif ( ! is_front_page() ) {\n\t\treturn;\n\t}\n\n\t\/\/ phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only display value, nothing is stored.\n\t$name = isset( $_GET['lw_name'] ) ? sanitize_text_field( wp_unslash( $_GET['lw_name'] ) ) : '';\n\tif ( '' === $name ) {\n\t\t$name = __( 'Guest', 'lw-hello-banner' );\n\t}\n\n\t$greeting = sprintf(\n\t\t\/* translators: %s: visitor name *\/\n\t\t__( 'Hello, %s!', 'lw-hello-banner' ),\n\t\t$name\n\t);\n\n\t$note = get_option( 'lw_hello_banner_note', '' );\n\n\techo '&lt;div class=\"lw-hello-banner\"&gt;&lt;p&gt;' . esc_html( $greeting );\n\tif ( '' !== $note ) {\n\t\techo ' ' . esc_html( $note );\n\t}\n\techo '&lt;\/p&gt;&lt;button type=\"button\"&gt;' . esc_html__( 'Dismiss', 'lw-hello-banner' ) . '&lt;\/button&gt;&lt;\/div&gt;';\n}\nadd_action( 'wp_body_open', 'lw_hello_banner_render' );<\/code><\/pre>\n<p>The name from the URL is kept on purpose, in the parameter <code>lw_name<\/code>: WordPress itself reads <code>name<\/code> as a post slug, so <code>?name=Anna<\/code> turns the front page into a 404 page on which <code>is_front_page()<\/code> is false. It is unslashed and sanitized, and the remaining Nonce warning is suppressed with a <code>phpcs:ignore<\/code> comment that names the sniff and gives a reason. The Plugin Check FAQ describes such annotations as the intended way to handle PHPCS false positives on a specific line. The justification is valid here only because the value changes nothing: it is not stored and it does not trigger an action. For a form that saves data, the correct fix is a Nonce plus a capability check, which <a href=\"https:\/\/www.lukaswojcik.com\/blog\/en\/wordpress-plugins-tricks\/sanitizing-escaping-nonces-capabilities\/\">a separate article in this series<\/a> covers in detail.<\/p>\n<pre><code>=== LW Hello Banner ===\nContributors: lukaswojcik\nTags: banner, greeting\nRequires at least: 6.3\nTested up to: 7.1\nRequires PHP: 7.4\nStable tag: 0.2.0\nLicense: GPLv2 or later\nLicense URI: https:\/\/www.gnu.org\/licenses\/gpl-2.0.html\n\nShows a short, dismissible greeting banner at the top of the front page.\n\n== Description ==\n\nLW Hello Banner prints a greeting on the front page. An optional note can be set under Settings &gt; General.\n\n== Changelog ==\n\n= 0.2.0 =\n* Fix all findings reported by Plugin Check.\n\n= 0.1.0 =\n* Initial version.<\/code><\/pre>\n<p>Two readme details follow from the check code. \u201cTested up to\u201d must match the current major version of WordPress, at the moment 7.1. An older value is an error for new plugins and a warning in update mode, and a value with a patch level such as 7.1.2 is reported as <code>invalid_tested_upto_minor<\/code>. The license in the readme is compared with the one in the plugin header and reported as <code>license_mismatch<\/code> if they differ, which is why both files use the same wording. The fields of the readme and the plugin header are the subject of <span class=\"lw-artikel-geplant\">a separate article in this series<\/span>.<\/p>\n<pre><code>\/\/ assets\/lw-hello-banner.js\n( function () {\n\tconst banner = document.querySelector( '.lw-hello-banner' );\n\tif ( ! banner ) {\n\t\treturn;\n\t}\n\n\tconst button = banner.querySelector( 'button' );\n\tif ( button ) {\n\t\tbutton.addEventListener( 'click', function () {\n\t\t\tbanner.hidden = true;\n\t\t} );\n\t}\n} )();<\/code><\/pre>\n<h2>Plugin Check in GitHub Actions<\/h2>\n<p>The official integration is the repository WordPress\/plugin-check-action. The latest release is v1.1.9 from 11 August 2026, and the moving tag <code>v1<\/code> currently points to the same commit. The action is a composite action. It sets up Node 24, installs <code>@wordpress\/env<\/code>, starts a WordPress instance with the plugin folder mapped into <code>wp-content\/plugins<\/code>, installs Plugin Check from the directory, installs any plugins listed in <code>Requires Plugins<\/code> and then calls <code>wp plugin check<\/code> with <code>--format=json<\/code> and <code>--require=.\/wp-content\/plugins\/plugin-check\/cli.php<\/code>, so runtime checks are included. According to the wp-env documentation, wp-env runs on Docker by default; the standard Ubuntu runner is therefore the natural choice.<\/p>\n<p>A Node script evaluates the JSON file. Each result becomes a file annotation with the result code as title. Any <code>ERROR<\/code> sets the exit code to 1 and fails the job, warnings only appear as annotations. With <code>strict: true<\/code> every result counts as an error. For pull requests the action also posts or updates a summary comment, and the raw result is uploaded as the artifact <code>plugin-check-results<\/code>.<\/p>\n<p>Inputs in v1.1.9: <code>build-dir<\/code> (default <code>.\/<\/code>), <code>checks<\/code>, <code>exclude-checks<\/code>, <code>categories<\/code>, <code>exclude-files<\/code>, <code>exclude-directories<\/code>, <code>ignore-codes<\/code>, <code>ignore-warnings<\/code>, <code>ignore-errors<\/code>, <code>include-experimental<\/code>, <code>wp-version<\/code> (<code>latest<\/code> or <code>trunk<\/code>), <code>severity<\/code>, <code>error-severity<\/code>, <code>warning-severity<\/code>, <code>include-low-severity-errors<\/code>, <code>include-low-severity-warnings<\/code>, <code>slug<\/code>, <code>strict<\/code> and <code>repo-token<\/code>. List inputs take one entry per line.<\/p>\n<p>Two details decide whether the results make sense. The action takes the slug from the last part of <code>build-dir<\/code>. With the default <code>.\/<\/code> that is the name of the checkout directory, which according to the GitHub documentation is named after the repository, as in <code>\/home\/runner\/work\/my-repo-name\/my-repo-name<\/code>; if the repository is named differently from the plugin, every text domain call is flagged. And the check <code>file_type<\/code> in 2.1.0 warns about a <code>.github<\/code> directory inside the plugin and flags other hidden files. Checking the repository root therefore tests files that never ship. The workflow below builds the folder that would be uploaded with <code>git archive<\/code>, which honours <code>export-ignore<\/code> entries in <code>.gitattributes<\/code>, and passes that folder as <code>build-dir<\/code>.<\/p>\n<pre><code># .github\/workflows\/plugin-check.yml\nname: Plugin Check\n\non:\n  pull_request:\n  push:\n    branches:\n      - main\n\npermissions:\n  contents: read\n  pull-requests: write # only for the summary comment on pull requests\n\njobs:\n  plugin-check:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions\/checkout@v7\n\n      # Build the folder that would be shipped, without .github and other\n      # development files (see export-ignore in .gitattributes).\n      - name: Build distributable plugin folder\n        run: |\n          mkdir -p build\n          git archive --format=tar --prefix=lw-hello-banner\/ HEAD | tar -x -C build\n\n      - name: Run Plugin Check\n        uses: wordpress\/plugin-check-action@v1\n        with:\n          # The folder name becomes the slug (text domain check, readme check).\n          build-dir: .\/build\/lw-hello-banner\n          wp-version: latest\n          # Optional filters, one entry per line:\n          # categories: |\n          #   plugin_repo\n          #   security\n          # exclude-checks: |\n          #   enqueued_scripts_scope\n          # ignore-codes: |\n          #   textdomain_mismatch\n          # Fail on warnings as well:\n          # strict: true<\/code><\/pre>\n<pre><code># .gitattributes: files that git archive leaves out of the build\n\/.gitattributes export-ignore\n\/.github        export-ignore\n\/.gitignore     export-ignore\n\/build          export-ignore<\/code><\/pre>\n<p>The permission <code>pull-requests: write<\/code> is only needed for the summary comment; according to GitHub&#8217;s permission tables the comment endpoint is covered by it. Without it the action logs a warning for the comment but still evaluates the results. Teams that prefer fixed versions can reference <code>wordpress\/plugin-check-action@v1.1.9<\/code> or its commit hash instead of <code>v1<\/code>.<\/p>\n<h2>Limits and open points<\/h2>\n<ul>\n<li>Plugin Check does not replace the manual review. The plugin page says so explicitly, and a clean run is no guarantee of approval.<\/li>\n<li>The findings table for the sample plugin comes from a local run of <code>wp plugin check<\/code> on WordPress 7.1.2. The workflow itself did not run on GitHub; locally, the <code>git archive<\/code> step produced the expected folder, and the evaluation script of the action (v1.1.9) ended with exit code 1 for the faulty and 0 for the corrected version.<\/li>\n<li>Some checks rely on heuristics. The prefix check derives the expected prefixes from the plugin code, which is why a consistent prefix such as <code>lw_hello_banner_<\/code> matters.<\/li>\n<li>The action installs whatever Plugin Check version the directory offers at run time and has no input to pin it. A pipeline can therefore turn red after a Plugin Check release without any change to the plugin.<\/li>\n<li>The action offers no input for the PHP version, and <code>wp-version<\/code> distinguishes only between the latest release and trunk. Tests against older WordPress or PHP versions need a separate setup, for example with wp-env or Playground, the topic of <span class=\"lw-artikel-geplant\">a separate article in this series<\/span>.<\/li>\n<li>Performance findings about script scope depend on how the plugin enqueues files; <a href=\"https:\/\/www.lukaswojcik.com\/blog\/en\/wordpress-plugins-tricks\/tutorials-en-wordpress-plugins-tricks\/performance-optimization-via-functions-php-selective-dequeuing-of-wordpress\/\">the article on selective dequeuing of plugin scripts<\/a> shows the other side of the problem.<\/li>\n<\/ul>\n<div class=\"lw-faq\">\n<h2>Questions and answers<\/h2>\n<h3>Does a run without findings guarantee that the plugin is accepted into the directory?<\/h3>\n<p>No. According to the Plugin Check FAQ a plugin typically has to pass all checks in the <code>plugin_repo<\/code> category, but approval still depends on the manual review. The plugin page states explicitly that Plugin Check is no replacement for the manual review; a clean run only helps to speed it up.<\/p>\n<h3>Why does the GitHub Action report more than a local wp plugin check run?<\/h3>\n<p>There are three typical reasons:<\/p>\n<ul>\n<li>The action always loads <code>cli.php<\/code> via <code>--require<\/code> and activates the plugin; locally, only static checks run without that workaround or while the plugin is inactive.<\/li>\n<li>The action installs the current Plugin Check version from the directory, while an older one may be active locally.<\/li>\n<li>With <code>build-dir: .\/<\/code> it checks the whole repository, including <code>.github<\/code> and hidden files.<\/li>\n<\/ul>\n<h3>When is it acceptable to suppress a warning?<\/h3>\n<p>When the finding is demonstrably harmless in the specific case, for example a sanitized URL parameter that is only displayed and never stored. A <code>phpcs:ignore<\/code> comment with the sniff name and a reason then belongs on that line. <code>--ignore-codes<\/code> or the action input <code>ignore-codes<\/code> hide a code across the whole plugin and therefore also hide real problems.<\/p>\n<h3>Why is the same text domain reported twice?<\/h3>\n<p>Two checks compare against the slug: <code>plugin_header_fields<\/code> checks the <code>Text Domain<\/code> header and reports <code>textdomain_mismatch<\/code> as a warning, <code>i18n_usage<\/code> checks every translation call and reports <code>WordPress.WP.I18n.TextDomainMismatch<\/code>. The slug is the folder name; in CI it can be set with the <code>slug<\/code> input, locally with <code>--slug<\/code>.<\/p>\n<\/div>\n<div class=\"lw-quellen\">\n<h2>Sources<\/h2>\n<ul>\n<li><a href=\"https:\/\/wordpress.org\/plugins\/plugin-check\/\" target=\"_blank\" rel=\"noopener noreferrer\">Plugin Check (PCP) on WordPress.org<\/a>, retrieved 29.09.2026<\/li>\n<li><a href=\"https:\/\/github.com\/WordPress\/plugin-check\" target=\"_blank\" rel=\"noopener noreferrer\">WordPress\/plugin-check, README<\/a>, retrieved 29.09.2026<\/li>\n<li><a href=\"https:\/\/github.com\/WordPress\/plugin-check\/releases\/tag\/2.1.0\" target=\"_blank\" rel=\"noopener noreferrer\">Plugin Check release 2.1.0<\/a>, retrieved 29.09.2026<\/li>\n<li><a href=\"https:\/\/github.com\/WordPress\/plugin-check\/blob\/2.1.0\/docs\/CLI.md\" target=\"_blank\" rel=\"noopener noreferrer\">Plugin Check: WP-CLI documentation (docs\/CLI.md)<\/a>, retrieved 29.09.2026<\/li>\n<li><a href=\"https:\/\/github.com\/WordPress\/plugin-check\/blob\/2.1.0\/docs\/checks.md\" target=\"_blank\" rel=\"noopener noreferrer\">Plugin Check: available checks (docs\/checks.md)<\/a>, retrieved 29.09.2026<\/li>\n<li><a href=\"https:\/\/github.com\/WordPress\/plugin-check\/blob\/2.1.0\/includes\/Checker\/Default_Check_Repository.php\" target=\"_blank\" rel=\"noopener noreferrer\">Default_Check_Repository.php (tag 2.1.0)<\/a>, retrieved 29.09.2026<\/li>\n<li><a href=\"https:\/\/github.com\/WordPress\/plugin-check\/blob\/2.1.0\/includes\/Checker\/Check_Categories.php\" target=\"_blank\" rel=\"noopener noreferrer\">Check_Categories.php (tag 2.1.0)<\/a>, retrieved 29.09.2026<\/li>\n<li><a href=\"https:\/\/github.com\/WordPress\/plugin-check\/blob\/2.1.0\/includes\/CLI\/Plugin_Check_Command.php\" target=\"_blank\" rel=\"noopener noreferrer\">Plugin_Check_Command.php (tag 2.1.0)<\/a>, retrieved 29.09.2026<\/li>\n<li><a href=\"https:\/\/github.com\/WordPress\/plugin-check\/blob\/2.1.0\/phpcs-rulesets\/plugin-check.ruleset.xml\" target=\"_blank\" rel=\"noopener noreferrer\">plugin-check.ruleset.xml (tag 2.1.0)<\/a>, retrieved 29.09.2026<\/li>\n<li><a href=\"https:\/\/github.com\/WordPress\/plugin-check\/tree\/2.1.0\/includes\/Checker\/Checks\" target=\"_blank\" rel=\"noopener noreferrer\">Check sources (tag 2.1.0)<\/a>, retrieved 29.09.2026<\/li>\n<li><a href=\"https:\/\/github.com\/WordPress\/plugin-check\/tree\/2.1.0\/tests\/phpunit\/tests\/Checker\/Checks\" target=\"_blank\" rel=\"noopener noreferrer\">Unit tests of the checks (tag 2.1.0)<\/a>, retrieved 29.09.2026<\/li>\n<li><a href=\"https:\/\/github.com\/WordPress\/plugin-check-action\" target=\"_blank\" rel=\"noopener noreferrer\">WordPress\/plugin-check-action, README<\/a>, retrieved 29.09.2026<\/li>\n<li><a href=\"https:\/\/github.com\/WordPress\/plugin-check-action\/blob\/v1.1.9\/action.yml\" target=\"_blank\" rel=\"noopener noreferrer\">plugin-check-action: action.yml (v1.1.9)<\/a>, retrieved 29.09.2026<\/li>\n<li><a href=\"https:\/\/github.com\/WordPress\/plugin-check-action\/blob\/v1.1.9\/src\/main.ts\" target=\"_blank\" rel=\"noopener noreferrer\">plugin-check-action: src\/main.ts (v1.1.9)<\/a>, retrieved 29.09.2026<\/li>\n<li><a href=\"https:\/\/github.com\/WordPress\/plugin-check-action\/releases\/tag\/v1.1.9\" target=\"_blank\" rel=\"noopener noreferrer\">plugin-check-action release v1.1.9<\/a>, retrieved 29.09.2026<\/li>\n<li><a href=\"https:\/\/developer.wordpress.org\/reference\/functions\/wp_enqueue_script\/\" target=\"_blank\" rel=\"noopener noreferrer\">wp_enqueue_script() \u2013 Developer Resources<\/a>, retrieved 29.09.2026<\/li>\n<li><a href=\"https:\/\/developer.wordpress.org\/block-editor\/reference-guides\/packages\/packages-env\/\" target=\"_blank\" rel=\"noopener noreferrer\">@wordpress\/env \u2013 Block Editor Handbook<\/a>, retrieved 29.09.2026<\/li>\n<li><a href=\"https:\/\/api.wordpress.org\/core\/version-check\/1.7\/\" target=\"_blank\" rel=\"noopener noreferrer\">WordPress.org version check API (current release 7.1.2)<\/a>, retrieved 29.09.2026<\/li>\n<li><a href=\"https:\/\/github.com\/actions\/checkout\/releases\" target=\"_blank\" rel=\"noopener noreferrer\">actions\/checkout releases (v7.0.1)<\/a>, retrieved 29.09.2026<\/li>\n<li><a href=\"https:\/\/docs.github.com\/en\/actions\/reference\/workflows-and-actions\/variables\" target=\"_blank\" rel=\"noopener noreferrer\">GitHub Docs: variables reference (GITHUB_WORKSPACE)<\/a>, retrieved 29.09.2026<\/li>\n<li><a href=\"https:\/\/docs.github.com\/en\/rest\/authentication\/permissions-required-for-github-apps\" target=\"_blank\" rel=\"noopener noreferrer\">GitHub Docs: permissions required for GitHub Apps<\/a>, retrieved 29.09.2026<\/li>\n<\/ul>\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>How Plugin Check 2.1.0 runs in the admin screen, via WP-CLI and in GitHub Actions, shown on a deliberately broken sample plugin and its corrected version.<\/p>\n","protected":false},"author":1,"featured_media":20887,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[92642],"tags":[91144,91093,91219,91115,91096],"class_list":["post-20854","post","type-post","status-publish","format-standard","hentry","category-tutorials-en-wordpress-plugins-tricks","tag-devops","tag-php","tag-tutorial","tag-web-security","tag-wordpress"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v28.1 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>Plugin Check locally and in CI: findings before the review | Lukas Wojcik<\/title>\n<meta name=\"description\" content=\"Plugin Check 2.1.0 with wp plugin check and the official GitHub Action: categories, result codes and a faulty sample plugin with its fix.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/www.lukaswojcik.com\/blog\/en\/wordpress-plugins-tricks\/tutorials-en-wordpress-plugins-tricks\/plugin-check-ci\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Plugin Check locally and in CI: findings before the review | Lukas Wojcik\" \/>\n<meta property=\"og:description\" content=\"Plugin Check 2.1.0 with wp plugin check and the official GitHub Action: categories, result codes and a faulty sample plugin with its fix.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/www.lukaswojcik.com\/blog\/en\/wordpress-plugins-tricks\/tutorials-en-wordpress-plugins-tricks\/plugin-check-ci\/\" \/>\n<meta property=\"og:site_name\" content=\"Lukas Wojcik - Blog\" \/>\n<meta property=\"article:published_time\" content=\"2026-10-07T07:14:00+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/09\/fi-20854-plugin-check-ci.png\" \/>\n\t<meta property=\"og:image:width\" content=\"1200\" \/>\n\t<meta property=\"og:image:height\" content=\"630\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/png\" \/>\n<meta name=\"author\" content=\"Lukas Wojcik\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Lukas Wojcik\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"19 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/wordpress-plugins-tricks\\\/tutorials-en-wordpress-plugins-tricks\\\/plugin-check-ci\\\/#article\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/wordpress-plugins-tricks\\\/tutorials-en-wordpress-plugins-tricks\\\/plugin-check-ci\\\/\"},\"author\":{\"name\":\"Lukas Wojcik\",\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/#\\\/schema\\\/person\\\/895f7604f9b6b71aad9bba33af28d0f9\"},\"headline\":\"Plugin Check locally and in CI: findings before the review\",\"datePublished\":\"2026-10-07T07:14:00+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/wordpress-plugins-tricks\\\/tutorials-en-wordpress-plugins-tricks\\\/plugin-check-ci\\\/\"},\"wordCount\":2521,\"commentCount\":0,\"publisher\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/#\\\/schema\\\/person\\\/895f7604f9b6b71aad9bba33af28d0f9\"},\"image\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/wordpress-plugins-tricks\\\/tutorials-en-wordpress-plugins-tricks\\\/plugin-check-ci\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/09\\\/fi-20854-plugin-check-ci.png\",\"keywords\":[\"DevOps\",\"PHP\",\"Tutorial\",\"Web Security\",\"WordPress\"],\"articleSection\":[\"Tutorials\"],\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/wordpress-plugins-tricks\\\/tutorials-en-wordpress-plugins-tricks\\\/plugin-check-ci\\\/#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/wordpress-plugins-tricks\\\/tutorials-en-wordpress-plugins-tricks\\\/plugin-check-ci\\\/\",\"url\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/wordpress-plugins-tricks\\\/tutorials-en-wordpress-plugins-tricks\\\/plugin-check-ci\\\/\",\"name\":\"Plugin Check locally and in CI: findings before the review | Lukas Wojcik\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/wordpress-plugins-tricks\\\/tutorials-en-wordpress-plugins-tricks\\\/plugin-check-ci\\\/#primaryimage\"},\"image\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/wordpress-plugins-tricks\\\/tutorials-en-wordpress-plugins-tricks\\\/plugin-check-ci\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/09\\\/fi-20854-plugin-check-ci.png\",\"datePublished\":\"2026-10-07T07:14:00+00:00\",\"description\":\"Plugin Check 2.1.0 with wp plugin check and the official GitHub Action: categories, result codes and a faulty sample plugin with its fix.\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/wordpress-plugins-tricks\\\/tutorials-en-wordpress-plugins-tricks\\\/plugin-check-ci\\\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/wordpress-plugins-tricks\\\/tutorials-en-wordpress-plugins-tricks\\\/plugin-check-ci\\\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/wordpress-plugins-tricks\\\/tutorials-en-wordpress-plugins-tricks\\\/plugin-check-ci\\\/#primaryimage\",\"url\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/09\\\/fi-20854-plugin-check-ci.png\",\"contentUrl\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/09\\\/fi-20854-plugin-check-ci.png\",\"width\":1200,\"height\":630,\"caption\":\"Plugin Check locally and in CI: findings before the review\"},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/wordpress-plugins-tricks\\\/tutorials-en-wordpress-plugins-tricks\\\/plugin-check-ci\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Plugin Check locally and in CI: findings before the review\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/#website\",\"url\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/\",\"name\":\"Lukas Wojcik - Blog\",\"description\":\"\",\"publisher\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/#\\\/schema\\\/person\\\/895f7604f9b6b71aad9bba33af28d0f9\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":[\"Person\",\"Organization\"],\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/#\\\/schema\\\/person\\\/895f7604f9b6b71aad9bba33af28d0f9\",\"name\":\"Lukas Wojcik\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/lw-x2.jpg\",\"url\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/lw-x2.jpg\",\"contentUrl\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/lw-x2.jpg\",\"width\":424,\"height\":636,\"caption\":\"Lukas Wojcik\"},\"logo\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/lw-x2.jpg\"},\"sameAs\":[\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\"]}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Plugin Check locally and in CI: findings before the review | Lukas Wojcik","description":"Plugin Check 2.1.0 with wp plugin check and the official GitHub Action: categories, result codes and a faulty sample plugin with its fix.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/www.lukaswojcik.com\/blog\/en\/wordpress-plugins-tricks\/tutorials-en-wordpress-plugins-tricks\/plugin-check-ci\/","og_locale":"en_US","og_type":"article","og_title":"Plugin Check locally and in CI: findings before the review | Lukas Wojcik","og_description":"Plugin Check 2.1.0 with wp plugin check and the official GitHub Action: categories, result codes and a faulty sample plugin with its fix.","og_url":"https:\/\/www.lukaswojcik.com\/blog\/en\/wordpress-plugins-tricks\/tutorials-en-wordpress-plugins-tricks\/plugin-check-ci\/","og_site_name":"Lukas Wojcik - Blog","article_published_time":"2026-10-07T07:14:00+00:00","og_image":[{"width":1200,"height":630,"url":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/09\/fi-20854-plugin-check-ci.png","type":"image\/png"}],"author":"Lukas Wojcik","twitter_card":"summary_large_image","twitter_misc":{"Written by":"Lukas Wojcik","Est. reading time":"19 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/www.lukaswojcik.com\/blog\/en\/wordpress-plugins-tricks\/tutorials-en-wordpress-plugins-tricks\/plugin-check-ci\/#article","isPartOf":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/en\/wordpress-plugins-tricks\/tutorials-en-wordpress-plugins-tricks\/plugin-check-ci\/"},"author":{"name":"Lukas Wojcik","@id":"https:\/\/www.lukaswojcik.com\/blog\/#\/schema\/person\/895f7604f9b6b71aad9bba33af28d0f9"},"headline":"Plugin Check locally and in CI: findings before the review","datePublished":"2026-10-07T07:14:00+00:00","mainEntityOfPage":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/en\/wordpress-plugins-tricks\/tutorials-en-wordpress-plugins-tricks\/plugin-check-ci\/"},"wordCount":2521,"commentCount":0,"publisher":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/#\/schema\/person\/895f7604f9b6b71aad9bba33af28d0f9"},"image":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/en\/wordpress-plugins-tricks\/tutorials-en-wordpress-plugins-tricks\/plugin-check-ci\/#primaryimage"},"thumbnailUrl":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/09\/fi-20854-plugin-check-ci.png","keywords":["DevOps","PHP","Tutorial","Web Security","WordPress"],"articleSection":["Tutorials"],"inLanguage":"en-US","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/www.lukaswojcik.com\/blog\/en\/wordpress-plugins-tricks\/tutorials-en-wordpress-plugins-tricks\/plugin-check-ci\/#respond"]}]},{"@type":"WebPage","@id":"https:\/\/www.lukaswojcik.com\/blog\/en\/wordpress-plugins-tricks\/tutorials-en-wordpress-plugins-tricks\/plugin-check-ci\/","url":"https:\/\/www.lukaswojcik.com\/blog\/en\/wordpress-plugins-tricks\/tutorials-en-wordpress-plugins-tricks\/plugin-check-ci\/","name":"Plugin Check locally and in CI: findings before the review | Lukas Wojcik","isPartOf":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/#website"},"primaryImageOfPage":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/en\/wordpress-plugins-tricks\/tutorials-en-wordpress-plugins-tricks\/plugin-check-ci\/#primaryimage"},"image":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/en\/wordpress-plugins-tricks\/tutorials-en-wordpress-plugins-tricks\/plugin-check-ci\/#primaryimage"},"thumbnailUrl":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/09\/fi-20854-plugin-check-ci.png","datePublished":"2026-10-07T07:14:00+00:00","description":"Plugin Check 2.1.0 with wp plugin check and the official GitHub Action: categories, result codes and a faulty sample plugin with its fix.","breadcrumb":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/en\/wordpress-plugins-tricks\/tutorials-en-wordpress-plugins-tricks\/plugin-check-ci\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/www.lukaswojcik.com\/blog\/en\/wordpress-plugins-tricks\/tutorials-en-wordpress-plugins-tricks\/plugin-check-ci\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/www.lukaswojcik.com\/blog\/en\/wordpress-plugins-tricks\/tutorials-en-wordpress-plugins-tricks\/plugin-check-ci\/#primaryimage","url":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/09\/fi-20854-plugin-check-ci.png","contentUrl":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/09\/fi-20854-plugin-check-ci.png","width":1200,"height":630,"caption":"Plugin Check locally and in CI: findings before the review"},{"@type":"BreadcrumbList","@id":"https:\/\/www.lukaswojcik.com\/blog\/en\/wordpress-plugins-tricks\/tutorials-en-wordpress-plugins-tricks\/plugin-check-ci\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/www.lukaswojcik.com\/blog\/"},{"@type":"ListItem","position":2,"name":"Plugin Check locally and in CI: findings before the review"}]},{"@type":"WebSite","@id":"https:\/\/www.lukaswojcik.com\/blog\/#website","url":"https:\/\/www.lukaswojcik.com\/blog\/","name":"Lukas Wojcik - Blog","description":"","publisher":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/#\/schema\/person\/895f7604f9b6b71aad9bba33af28d0f9"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/www.lukaswojcik.com\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":["Person","Organization"],"@id":"https:\/\/www.lukaswojcik.com\/blog\/#\/schema\/person\/895f7604f9b6b71aad9bba33af28d0f9","name":"Lukas Wojcik","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/07\/lw-x2.jpg","url":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/07\/lw-x2.jpg","contentUrl":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/07\/lw-x2.jpg","width":424,"height":636,"caption":"Lukas Wojcik"},"logo":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/07\/lw-x2.jpg"},"sameAs":["https:\/\/www.lukaswojcik.com\/blog"]}]}},"_links":{"self":[{"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/posts\/20854","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/comments?post=20854"}],"version-history":[{"count":1,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/posts\/20854\/revisions"}],"predecessor-version":[{"id":21691,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/posts\/20854\/revisions\/21691"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/media\/20887"}],"wp:attachment":[{"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/media?parent=20854"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/categories?post=20854"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/tags?post=20854"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}