WP Manifestindependent plugin directory
manifest / developer / lh-github-publisher

LH GitHub Publisher

Publishes Peter's own LocalHero plugins to GitHub, and triages/contributes fixes to third-party repos (fork, propose, apply, PR). Network-activated on Multisite.

★ 0stars

Install

Download source zip No release zip yet

This is the repository archive. It installs, but the folder name will carry the branch suffix and updates will not flow.

Install straight from GitHub with WP-CLI:

wp plugin install https://github.com/lhero-org/lh-github-publisher/archive/refs/heads/main.zip
No built-in updater. New versions won't appear under Dashboard → Updates. Install new releases yourself, or let Git Updater track the repository.

Publishes Peter's own LocalHero plugins to GitHub, and triages/contributes fixes to third-party GitHub repos Peter doesn't own (fork → propose → apply → pull request).

This plugin replaces LH GitHub (lh-github, retired at v2.0). It is treated as a new plugin — fresh version numbering starting at 1.00 — though it carries forward the working publish logic from lh-github unchanged. Not to be confused with LH Git Updater (lh-git-updater), which pulls plugin updates from GitHub down onto the site. This plugin pushes/contributes in the other direction.

Network-activated on Multisite.

Installation

  1. Deactivate and delete the old lh-github plugin if present (this plugin supersedes it — running both will register duplicate admin menus).
  2. Upload this plugin's zip via Network Admin → Plugins → Add New → Upload Plugin, or extract to wp-content/plugins/lh-github-publisher/.
  3. Network-activate.
  4. Go to Network Admin → Settings → LH GitHub Publisher and enter:
    • A GitHub Personal Access Token with repo scope (add workflow scope too if you'll ever touch .github/workflows/* files via propose/apply).
    • Your default GitHub org/user (used for your own plugin repos).
  5. Settings are encrypted at rest (AES-256-CBC, key derived from WP salts) in site options lh_ghp_pat / lh_ghp_org.

Admin interface

Network Admin → Settings → LH GitHub Publisher (settings.php?page=lh-github-publisher)

  • Settings form: PAT + default org.
  • Plugin table: every installed plugin with a README.md/readme.md, showing local version, GitHub version (1-hour cached, refreshable), sync status, repo link, last-push SHA/date, and a Publish/Update button. Publish creates the GitHub repo if missing; both force-push current disk state via git. Refuses to push if the local version number matches what's already on GitHub — bump the version first.

This admin UI only covers Peter's own repos. Upstream triage/contribution (issues, forks, PRs) is MCP-only by design — there's no admin UI for it.

Database

No custom tables. Two encrypted site options (lh_ghp_pat, lh_ghp_org) plus one site option holding pending/applied upstream-change proposals (lh_ghp_upstream_proposals, capped at the 20 most recent, oldest dropped first — this is a staging area, not an audit log). Transients cache the authenticated GitHub login and per-plugin GitHub version for an hour.

No cron jobs.

Hooks

  • lh-github-publisher_init_plugin (filter, default true) — return false to prevent the plugin from initialising.
  • lh-github-publisher_return_plugin_namespace, lh-github-publisher_return_plugin_text_domain, lh-github-publisher_plugin_name, lh-github-publisher_plugin_version — filterable identity methods, standard across all LH plugins.
  • MCP: wp_abilities_api_categories_init (registers the lh-github-publisher category), wp_abilities_api_init (registers all abilities below).

MCP abilities

All abilities are gated to the install_plugins capability and are namespaced under lh-github-publisher/.

Own repos (disk canonical):

  • push-plugin — force-push an installed plugin to GitHub, creating the repo if needed. Refuses if the local version is already on GitHub.
  • get-file-contents — read a file from any GitHub repo via the stored PAT.

Triage (GitHub canonical):

  • list-issues — list or full-text search issues on a repo. Always check before filing.
  • create-issue-comment — comment on an existing issue or PR.
  • create-issue — file a new issue.

Contribute (GitHub canonical, new territory for this plugin):

  • fork-repo — idempotent fork of an upstream repo to Peter's account or a given org. Safe to call repeatedly; recognises an existing fork by checking parent.full_name on the target repo rather than assuming.
  • propose-upstream-change — ensures a fork exists, creates/reuses a branch on it, reads current file contents from that branch, and stores a diff of the proposed change as a pending proposal. Nothing is committed by this call.
  • apply-upstream-change — commits an approved proposal to its fork branch. Requires confirmed:true on every call — Peter's explicit decision (2026-07-10) was to confirm every commit rather than only gating at PR time, even though the fork branch is otherwise disposable/invisible upstream until a PR opens.
  • create-pull-request — opens a PR from an applied proposal's fork branch against the upstream repo/base branch. Also requires confirmed:true, since this is the point a change becomes publicly visible to upstream maintainers.

Typical upstream-fix flow

  1. list-issues to confirm the bug and check for an existing fix PR.
  2. propose-upstream-change with the target file(s) and full proposed contents — returns a diff.
  3. Show the diff to Peter, get explicit approval.
  4. apply-upstream-change with confirmed:true.
  5. Confirm again, then create-pull-request with confirmed:true.

Known limitations (v1.02)

  • apply-upstream-change commits each file in the proposal as a separate Contents-API commit (not one atomic multi-file commit via the Git Data tree API). Fine for the common single-file-fix case; a multi-file proposal will produce one commit per file on the fork branch.
  • propose-upstream-change branches from the fork's current state of base_branch. If the fork has drifted behind upstream, the base SHA used will be stale. There's no fork-sync step yet — worth adding if forks start going stale in practice.
  • The inline diff generator is a self-contained LCS implementation (no xdiff dependency), guarded against pathologically large files (skips inline diff above ~4M line-pairs and says so).

Changelog

1.02 — 2026-07-10

  • Fixed unescaped output in the admin notice renderer (now uses printf() with esc_attr()/wp_kses_post() instead of raw string interpolation into echo).
  • Switched all wp_redirect() calls in the admin handlers to wp_safe_redirect().
  • Added a justified phpcs:ignore for the read-only $_GET['saved'] display flag — it only toggles a notice, the actual save is nonce-verified separately in handle_save_settings().
  • Added readme.txt (WP.org-standard plugin header format) alongside this readme.md. Plugin Check expects readme.txt independently of readme.md's documentation role.
  • Fixed a widespread bug present since v1.00: em dashes and arrows were stored as literal \u2014/\u2192 text (6-7 raw characters) instead of real Unicode characters, across this file and 9 PHP files. PHP doesn't interpret \uXXXX escapes outside \u{XXXX} syntax in double-quoted strings, so every instance rendered literally. All replaced with real characters.
  • All NonSingularStringLiteralDomain warnings from run-plugin-check are the accepted false positive from self::return_plugin_text_domain() — documented per LH plugin conventions, no code change.

1.01 — 2026-07-10

  • Fixed LH_GHP_Github_Api::request() encoding an empty PHP array body as JSON [] instead of {}. This broke fork-repo whenever no fork_into_org was given — GitHub's POST .../forks endpoint (and others) reject [] for an empty object body with "Body should be a JSON object". Found via a live first run against WordPress/mcp-adapter.

1.00 — 2026-07-10

  • Initial release, superseding lh-github v2.0. Renamed for clarity against lh-git-updater.
  • Ported push-plugin and get-file-contents unchanged in behaviour.
  • Added list-issues, create-issue-comment, create-issue for third-party issue triage.
  • Added fork-repo, propose-upstream-change, apply-upstream-change, create-pull-request for upstream contribution, built to fix WordPress/mcp-adapter#182 as the first real-world case.
  • apply-upstream-change and create-pull-request both require explicit confirmed:true on every call (Peter's decision, confirm-every-time model rather than confirm-only-at-PR).