# Wordpress Github Plugin Updates

> Sets up private GitHub Releases auto-shipping and in-dashboard WordPress plugin updates with branch channels (main=stable, other=prerelease), lean installable zips, Plugin Update Checker, admin settings (repo/token/branch select), Test connection UI, and plugin details UX (local icons, banners, readme.txt). Use when adding GitHub-based plugin updates, auto-release workflows, WordPress updater integration, release zips without node_modules, View details polish, or when the user mentions Plugin Update Checker, GitHub Releases for WP plugins, or keeping the plugin folder name stable across updates.

- Skill: `kyleasobus/wordpress-github-plugin-updates` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add kyleasobus/wordpress-github-plugin-updates`
- Raw SKILL.md: https://api.skillmd.com/api/skills/kyleasobus/wordpress-github-plugin-updates/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- Author: kyleasobus (https://skillmd.com/u/kyleasobus)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/kyleasobus/wordpress-github-plugin-updates

---


# WordPress GitHub plugin updates

Implement private-repo auto-releases and WordPress in-dashboard updates using this proven pattern. Adapt names to the target plugin; do not hard-code Provider Connect.

## Goal

1. Bump `Version:` in the main plugin PHP file → push → GitHub Actions creates a release + lean zip.
2. Installed sites see the update via **Dashboard → Updates** / Plugins.
3. Plugin directory name stays `{plugin-slug}/` after update.
4. `dev` (or any non-`main`) releases do **not** affect production sites following `main`.

## Gather before coding

Ask if missing:

- GitHub `org/repo` (private or public)
- Main plugin file path and slug (folder name must match zip prefix)
- Whether Composer `vendor/` is gitignored (assume yes → **require release assets**)
- Production branch name (default `main`)

## Channel model (required)

| Push branch | Tag | GitHub release | Sites that see it |
|-------------|-----|----------------|-------------------|
| `main` | `v{Version}` | **stable** | Branch setting = `main` |
| other (e.g. `dev`) | `v{Version}-{branch}` | **prerelease** | Branch setting = that branch only |

WordPress sites following `main` must ignore prereleases. Non-main sites filter tags ending with `-{branch}`.

```mermaid
flowchart LR
  bump["Bump Version header"] --> push["Push branch"]
  push --> ci["GitHub Actions"]
  ci --> zip["Lean zip + release"]
  zip --> stable["main: stable vX.Y.Z"]
  zip --> pre["other: prerelease vX.Y.Z-branch"]
  stable --> prod["WP branch=main"]
  pre --> staging["WP branch=dev"]
```

## Implementation checklist

Copy and track:

```
- [ ] Lean zip packager (allowlist; root folder = plugin slug)
- [ ] GitHub Actions: version bump → tag → release + attach zip
- [ ] Composer dep: yahnis-elsts/plugin-update-checker
- [ ] updater.php: auth, release assets required, channel filters
- [ ] Force release strategy for non-main (PUC defaults to branch zip)
- [ ] Admin settings: repo, token, branch <select>, Test connection
- [ ] Update URI header → real GitHub repo URL
- [ ] Plugin header: clear short Description (+ Requires at least / Requires PHP as needed)
- [ ] WordPress-standard readme.txt (Description / Installation / FAQ / Changelog)
- [ ] Plugin icons under assets/ (icon.svg and/or icon-128x128.png, icon-256x256.png)
- [ ] Plugin banners under assets/ (banner-772x250.png, banner-1544x500.png)
- [ ] Updater: inject local icons (puc_pre_inject_update), banners + readme (puc_pre_inject_info)
- [ ] Zip allowlist includes assets/ and readme.txt
- [ ] Keep Stable tag in readme.txt in sync with Version header on release
- [ ] README: ship flow, channels, token scopes, constants
- [ ] .gitignore: vendor/, node_modules/, dist/
```

## 1. Lean zip

- Allowlist runtime files only (main PHP, `readme.txt`, `assets/` icons/banners, CSS/JS, `includes/`, `templates/`, `vendor/`, needed JSON). **Never** ship `node_modules`, `.git`, `scripts/` (unless required at runtime), tests, or CI files.
- Zip top-level folder **must** be `{plugin-slug}/` so updates replace the same directory.
- Run `composer install --no-dev` before zipping when `vendor/` is not in git.
- Name artifact `{plugin-slug}-{version}.zip`.

## 2. Auto-release workflow

Trigger on push when the main plugin file changes (path filter).

1. Parse `Version:` from the plugin header.
2. Tag: `v{version}` on `main`; `v{version}-{sanitized-branch}` otherwise.
3. Skip if tag already exists.
4. Build zip; create release with `softprops/action-gh-release`.
5. `prerelease: ${{ steps.meta.outputs.prerelease == 'true' }}` — string `"false"` is truthy in JS; compare explicitly.
6. Attach only the expected zip path.

Template: [reference.md](reference.md#github-actions-workflow).

## 3. WordPress updater

Use [Plugin Update Checker](https://github.com/YahnisElsts/plugin-update-checker) v5+.

Config precedence (highest first):

1. `wp-config.php` constants
2. Admin options
3. Filters

Required behavior:

- Authenticate with PAT for private repos (`repo` classic, or fine-grained Contents + Metadata read).
- `enableReleaseAssets('/^{slug}-.+\.zip$/i', REQUIRE_RELEASE_ASSETS)` — never fall back to source zip if `vendor/` is missing from git.
- **Branch `main`:** skip prereleases (PUC default).
- **Other branch:** `RELEASE_FILTER_ALL` + callback: tag ends with `-{branch}` and `prerelease` is true.
- **Critical:** PUC only uses release strategy for `main`/`master`. For other branches, filter `puc_vcs_update_detection_strategies-{slug}` to **only** `latest_release` (no branch archive).

Settings UI:

- Repository `org/repo`
- Access token (password; blank keep existing)
- Branch **`<select>`** populated after successful GitHub connection (list `/repos/.../branches`)
- **Test connection** AJAX: repo metadata, branch list, latest channel version, zip asset present?, installed version
- Constants lock corresponding fields when defined

Patterns: [reference.md](reference.md#wordpress-updater--settings).

## 4. Plugin header + docs

```php
 * Update URI: https://github.com/{org}/{repo}
```

Improve the short header `Description:` (Plugins list blurb). Optional headers: `Requires at least`, `Requires PHP`.

README must document: bump Version → push → workflow; channel rules; Settings vs constants; token scopes; folder name `{slug}`.

## 5. Plugin details UI (icons, banners, readme)

Give admins a proper **View details** experience for private-repo plugins.

### Why local injection

Private GitHub repos break the default “pretty” Plugins UI:

- WordPress / PUC may try to load icon/banner URLs from GitHub; those URLs **won’t load in the browser** without public access.
- The short `Description:` header alone is too thin for **View details**.
- PUC *can* fetch remote `readme.txt`, but local packaging + injection is more reliable for private repos and for sites that already have the zip installed.

**Pattern:** ship assets + `readme.txt` inside the lean zip, then inject them via PUC filters using `plugins_url()` from the installed plugin folder.

### Files to ship

| Path | Purpose |
|------|---------|
| `readme.txt` | WordPress.org-format readme; powers Description / Installation / FAQ / Changelog tabs |
| `assets/icon.svg` (optional) | SVG icon |
| `assets/icon-128x128.png` | 1x icon |
| `assets/icon-256x256.png` | 2x icon |
| `assets/banner-772x250.png` | Low-res banner |
| `assets/banner-1544x500.png` | High-res banner |

### `readme.txt` requirements

- First line: `=== {Plugin Display Name} ===`
- Metadata block: Contributors, Requires at least, Tested up to, Requires PHP, Stable tag, License
- One-line short description (≤150 chars preferred; parser truncates)
- Sections: `== Description ==`, `== Installation ==`, `== Frequently Asked Questions ==`, `== Changelog ==`, optional `== Upgrade Notice ==`
- Keep **Stable tag** aligned with the plugin `Version:` on release

PUC parses this with `PucReadmeParser` (bundled with Plugin Update Checker).

### Updater injection (required for private repos)

After `buildUpdateChecker` + auth, register local asset filters. **Do not rely on GitHub-hosted image URLs.**

```php
// Icons → update notices / Plugins list
add_filter('puc_pre_inject_update-{plugin-slug}', function ($update) use ($icons) {
	if (is_object($update)) {
		$update->icons = $icons; // keys: svg, 1x, 2x → plugins_url(...) values
	}
	return $update;
});

// Banners + readme sections → View details modal
add_filter('puc_pre_inject_info-{plugin-slug}', function ($info) use ($banners, $readme) {
	if (!is_object($info)) {
		return $info;
	}
	if ($banners) {
		$info->banners = $banners; // keys: low, high
	}
	if (!empty($readme['sections'])) {
		$info->sections = array_merge(
			is_array($info->sections ?? null) ? $info->sections : [],
			$readme['sections']
		);
	}
	// Optionally fill short_description / requires / tested / requires_php if empty
	return $info;
});
```

Implementation notes:

- Resolve files with `is_readable()` + `plugins_url($relative, $plugin_file)` so only present files are attached.
- Parse local `readme.txt` with `new \PucReadmeParser()` when the class exists.
- Prefer **packaged** readme sections over thin remote metadata (`array_merge` so local keys win).
- These filters only run when the updater is bootstrapped (repo + token configured). That matches when PUC’s View details hook is active.

Full patterns: [reference.md](reference.md#local-plugin-details-injection).

### Verify (details UI)

1. Deploy/sync plugin with assets + `readme.txt` present.
2. Configure GitHub updater (repo + token) so PUC is active.
3. **Plugins → View details** shows banner, Description/Installation/FAQ from readme.
4. Plugins list / update UI shows icon when available.
5. Release zip contains `assets/` and `readme.txt` under `{plugin-slug}/`.

## Anti-patterns

- Manual “draft a release” as the primary ship path
- Downloading GitHub source zip when Composer deps are required
- Same tag for `main` and `dev` (collides; always suffix non-main)
- Letting non-main use PUC branch download (breaks installs without `vendor/`)
- Changing zip root folder name across releases
- Pointing icon/banner URLs at private GitHub raw/blob links (admin browser can’t auth)
- Shipping only a long header `Description:` and skipping `readme.txt`
- Forgetting `readme.txt` / `assets/` in the zip allowlist (works in local clone, breaks after update)
- Letting `Stable tag` drift from `Version:`

## Verify

1. Push Version bump on `main` → stable release + zip asset.
2. Push Version bump on `dev` → prerelease `vX.Y.Z-dev` only.
3. Site branch=`main` Test connection → sees stable only.
4. Site branch=`dev` → sees `*-dev` prerelease only.
5. Update installs into `wp-content/plugins/{slug}/` (name unchanged).
6. View details shows local banner + readme sections; Plugins list shows icon.

## Sharing this skill with a team

See [sharing.md](sharing.md).

