Skip to content

The excluded-plugin setting that Playwright ignored — fixing browser-mode updates and false residual warnings

The excluded-plugin setting that Playwright ignored — fixing browser-mode updates and false residual warnings

The symptom

In browser-mode maintenance (Playwright, no SSH), plugins marked as “excluded from update checks” were still being updated. After the run, a “plugin updates remaining” WARNING email arrived every time.

The excluded plugins were intentionally left behind, but the residual check treated them as unfinished updates and fired a warning — a two-part problem: wrong behavior and a misleading alert.

SSH path vs. Playwright path

On SSH-capable sites, WP-CLI’s --skip-plugins flag carries the ignored_plugins list into the update command. That path already excluded them correctly.

The Playwright path was different. browser_update_remaining_plugins() worked by clicking the “select all” checkbox on update-core.php and submitting the form — no filtering at all.

# Before: select-all, ignored_plugins never consulted
page.check('input[name="action"][value="update-selected-plugins"]')
for cb in page.query_selector_all('input[name="checked[]"]'):
    cb.check()

This function is the chokepoint for two flows: pure browser-mode updates (run_browser_update_flow) and the browser residual pass that runs after SSH updates (run_browser_residual_update, on by default). So even SSH sites could have excluded plugins updated by the residual pass. Browser-driven bulk updates are also outside the scope of pinpoint rollback, meaning there is no automatic recovery if a wrong update goes through.

Fix 1 — _ignored_plugin_slugs() and _plugin_slug_from_checkbox_value()

When excluded plugins are configured, the fix replaces the select-all approach with per-checkbox evaluation.

def _ignored_plugin_slugs(site: dict) -> set[str]:
    raw = site.get("ignored_plugins", "")
    return {s.strip().lower() for s in raw.split(",") if s.strip()}

def _plugin_slug_from_checkbox_value(value: str) -> str:
    return value.split("/")[0].strip().lower() if "/" in value else value.strip().lower()

Plugin checkboxes on update-core.php use a value like contact-form-7/wp-contact-form-7.php. The slug is the part before the slash. Normalizing to lowercase covers casing differences in user input.

With excluded plugins configured:

ignored = _ignored_plugin_slugs(site)
if ignored:
    for cb in page.query_selector_all('input[name="checked[]"]'):
        val = cb.get_attribute("value") or ""
        slug = _plugin_slug_from_checkbox_value(val)
        if slug and slug not in ignored:
            cb.check()
        # if value is missing, keep it checked (safe side: update rather than skip)

Sites with no excluded plugins keep the original behavior (select-all with individual fallback) exactly as before.

Fix 2 — _browser_plugin_residual_state() stops false residual warnings

The residual check had the same gap. Before the fix, any remaining update form on update-core.php counted as “updates pending.” An excluded plugin left intentionally on the page was enough to trigger a WARNING.

def _browser_plugin_residual_state(page, site: dict) -> bool:
    ignored = _ignored_plugin_slugs(site)
    checkboxes = page.query_selector_all('input[name="checked[]"]')
    if not checkboxes:
        return False  # no form = nothing pending
    if not ignored:
        return True   # no exclusions = form present = pending (original behavior)
    for cb in checkboxes:
        val = cb.get_attribute("value") or ""
        slug = _plugin_slug_from_checkbox_value(val)
        if slug not in ignored:
            return True  # at least one non-excluded plugin still pending
    return False  # only excluded plugins remain — not pending

After this change, a site where only excluded plugins remain on update-core.php resolves to False (not pending), and no WARNING email is sent.

Test coverage

tests/test_browser_ignored_plugins.py — 21 new tests:

  • Slug normalization (7): slug/file.php format, single-file format, case and whitespace variation, missing value
  • Exclusion behavior (9): single exclusion, multiple exclusions, all excluded (zero updates), no exclusions (all updated), nonexistent slug in exclusion list does not over-exclude
  • Residual check (5): only excluded remain → not pending, excluded + non-excluded remain → pending, no exclusions (original behavior), no form → not pending

Related: The select-all checkbox that Playwright checked but the page ignored / Translating Playwright’s raw error messages into readable text