Securely install and update WordPress themes and plugins from GitHub, without FTP or SSH.
- Requires: WordPress 6.5+, PHP 8.0+
- Text domain:
gitpress - Main file:
gitpress.php
An administrator authenticates with GitHub via OAuth, registers a repository (URL + branch/tag/release + optional subdirectory), and the plugin detects whether it contains a WordPress plugin or theme, then installs or updates it using the WordPress upgrader infrastructure. Optional push webhooks deploy automatically after merges.
Local development → feature branch → pull request → merge into main
→ GitHub push webhook → signature validation → background deployment
→ validation → success or rollback
| Class | Responsibility |
|---|---|
Plugin |
Bootstrap, service wiring, activation/deactivation hooks |
Settings |
Options API persistence, encrypted token store |
Encryption |
Sodium secretbox keyed from AUTH_KEY + AUTH_SALT |
GitHubClient |
WordPress HTTP API wrapper for api.github.com |
GitHubAuth |
OAuth connect/callback, state + nonce |
Utils\Url / Slug / Ref / WebhookSignature / ReplayGuard |
Pure validators |
PackageInspector |
Safe archive extraction + header-only detection |
Installer |
Upgrader orchestration, rollback, locking |
RepositoryManager |
Managed repository CRUD |
UpdateChecker |
Twice-daily cron + background deploy action |
Webhook |
REST endpoint with HMAC + replay protection |
Logger |
Capped, sanitized audit log |
AdminUI |
Tools → GitPress screen |
- Copy
gitpressto/wp-content/plugins/and activate. - Create a GitHub OAuth App (see below) and add the credentials to
wp-config.php. - Open Tools → GitPress.
-
GitHub → Settings → Developer settings → OAuth Apps → New OAuth App.
-
Set Authorization callback URL to your site's
https://example.com/wp-admin/admin-post.php?action=gitpress_oauth_callback(the exact URL is shown on the plugin settings screen). -
Enter the credentials in one of two ways:
- On Tools → GitPress → GitHub OAuth App (stored in the database; the client secret is encrypted and never displayed), or
- In
wp-config.php, which takes precedence over the settings screen:
define( 'GITPRESS_GITHUB_CLIENT_ID', '...' );
define( 'GITPRESS_GITHUB_CLIENT_SECRET', '...' );The client secret is used server-side during the token exchange and is never
logged or rendered. Database credentials are encrypted when sodium and the
WordPress authentication salts are available; otherwise the dashboard warns
the administrator to prefer wp-config.php constants.
- Add a repository and enable automatic deployment.
- Copy the one-time webhook URL and secret shown for that repository.
- In GitHub, add a webhook with Content type
application/json, the secret, and Just the push event.
The plugin verifies X-Hub-Signature-256, ignores pushes to other branches,
deduplicates delivery IDs, and deploys in the background.
| Filter | Default | Purpose |
|---|---|---|
gitpress_api_timeout |
20 |
GitHub API request timeout (seconds) |
gitpress_download_timeout |
300 |
Archive download timeout (seconds) |
gitpress_max_archive_bytes |
50 * MB_IN_BYTES |
Maximum archive size |
gitpress_oauth_scopes |
repo |
OAuth scopes requested |
The plugin connects directly to GitHub for OAuth, account information, repository metadata, commits, releases, and source archives. OAuth exchanges send the configured client credentials, authorization code, callback URL, and scope. Authenticated API requests send the access token and configured repository identifiers/references. No data passes through a service operated by the plugin author.
gitpress_deployed— after a successful deployment ($type,$slug,$version,$sha).
- Token at rest — encrypted with libsodium secretbox using a key derived
from
AUTH_KEY+AUTH_SALT. If libsodium is missing, the token is stored in plaintext and the admin is warned. Tokens are never placed in transients, cookies, or local storage. - OAuth CSRF — a high-entropy
statevalue plus a WordPress nonce; state is verified withhash_equals()before accepting a token. - Webhook forgery — every request must carry a valid HMAC-SHA256
(
X-Hub-Signature-256) for that repository's secret. The repository, branch, and destination are always read from saved admin configuration, never from the payload. - Replay —
X-GitHub-DeliveryIDs are recorded and rejected if repeated. - ZIP Slip / traversal — archive entries with absolute paths,
..,\, or null bytes are rejected before extraction. - Code execution during inspection — detection only reads file headers
(first 8 KB) using the same algorithm as
get_file_data(); downloaded PHP is never executed. - SSRF — only
api.github.comand its archive endpoint are ever contacted; SSL verification is never disabled. - Privilege escalation — every administrative action requires
install_pluginsandinstall_themes, plus a nonce. - Accidental overwrite — the plugin refuses to replace itself, refuses to overwrite unmanaged packages without explicit confirmation, and refuses to touch a destination managed by a different entry.
- Data loss — a rollback copy is made before every replacement and restored on failure. A transient lock prevents concurrent deployments.
- The archive size limit is enforced after the download completes (with a
best-effort early rejection via
Content-Length); an oversized download is still consumed before being rejected. - If
AUTH_KEY/AUTH_SALTare changed, stored tokens can no longer be decrypted and the account must be reconnected. - The full OAuth flow and private-repository behavior require real GitHub OAuth App credentials and cannot be fully automated in CI.
secret_shownpersistence is best-effort; the secret is displayed once after the repository is created and then hidden.
- Activate the plugin with no warnings or fatal errors.
- Define
GITPRESS_GITHUB_CLIENT_ID/GITPRESS_GITHUB_CLIENT_SECRET, click Connect GitHub, and confirm the account appears. - Validate a public plugin repository (e.g.
WordPress/two-factor) and confirm the detected name/version/type/sha are shown. - Install the repository and confirm it appears in Plugins with the correct folder slug.
- Change the branch HEAD and click Deploy now; confirm the version updates and the folder slug is unchanged, and an active plugin stays active.
- Install a theme repository and confirm it appears under Appearance → Themes.
- Enable automatic deployment, add a webhook with the shown secret, and push to the configured branch; confirm a background deployment occurs.
- Push with a wrong secret (or no signature) and confirm a
401. - Push to a non-configured branch and confirm it is ignored.
- Confirm temporary files are removed after both successful and failed
operations (
lsin the system temp dir). - Force a failure (e.g. invalid branch) and confirm the previous version is restored.
composer install
vendor/bin/phpcs --standard=phpcs.xml.dist .
vendor/bin/phpunitPHPUnit tests cover URL parsing, slug validation, ref matching, signature verification, replay protection, and archive inspection. The test suite is dependency-light and does not require a running WordPress installation.
The distributable ZIP must contain a single top-level gitpress
directory and exclude development-only files (vendor/, composer.json,
composer.lock, tests/, phpcs.xml.dist, .git*).
GPLv2 or later.