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
This is a summary aggregated from Dev.to. Read the complete article on the original site:
Read full article at Dev.to