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.

by Peter Shaw · github.com/lhero-org/lh-github-publisher · website

0stars
0forks

Install

No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:

wp plugin install https://github.com/lhero-org/lh-github-publisher/archive/refs/heads/main.zip

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