A WordPress plugin for automatic updates from Forgejo. A lightweight alternative to Git Updater, with no banners and no external dependencies. https://emanuelegori.uno/en/plugins/eg-forgejo-updater/
Find a file
Emanuele Gori 0fdd48b47b
All checks were successful
Auto Release on version bump / release (push) Successful in 5s
Docs: install from the release ZIP; GitHub repository is a read-only mirror
2026-10-17 20:10:17 +02:00
.forgejo/workflows ci: workflow auto-release su bump versione (tag + note CHANGELOG + zip) 2026-06-15 22:34:04 +02:00
images v3.3.0 — Compatibility data and icon in updates, normalized Tested up to, per-plugin cache clearing 2026-10-11 19:20:59 +02:00
includes v3.3.1 — "Use WordPress.org" button, Documentation links to the plugin page 2026-10-11 22:31:18 +02:00
languages v3.3.1 — "Use WordPress.org" button, Documentation links to the plugin page 2026-10-11 22:31:18 +02:00
.distignore Fix phpcs:ignore echo $ok/$warn e nonce notice, aggiungi .distignore 2026-06-02 13:44:18 +02:00
.gitattributes build: add .gitattributes to exclude dev files from release archives 2026-08-09 16:43:27 +02:00
.gitignore feat: sicurezza, file standard WP e miglioramenti architetturali 2026-04-05 20:15:53 +02:00
CHANGELOG.md v3.3.1 — "Use WordPress.org" button, Documentation links to the plugin page 2026-10-11 22:31:18 +02:00
changelog.txt v3.0.0 — Per-instance access tokens, full prefixes, WordPress.org-style readme 2026-10-11 15:55:16 +02:00
eg-forgejo-updater.php v3.3.1 — "Use WordPress.org" button, Documentation links to the plugin page 2026-10-11 22:31:18 +02:00
index.php feat: sicurezza, file standard WP e miglioramenti architetturali 2026-04-05 20:15:53 +02:00
LICENSE feat: sicurezza, file standard WP e miglioramenti architetturali 2026-04-05 20:15:53 +02:00
README.it-IT.md Docs: install from the release ZIP; GitHub repository is a read-only mirror 2026-10-17 20:10:17 +02:00
README.md Docs: install from the release ZIP; GitHub repository is a read-only mirror 2026-10-17 20:10:17 +02:00
readme.txt Docs: install from the release ZIP; GitHub repository is a read-only mirror 2026-10-17 20:10:17 +02:00
uninstall.php v3.0.0 — Per-instance access tokens, full prefixes, WordPress.org-style readme 2026-10-11 15:55:16 +02:00

EG Forgejo Updater

🇬🇧 English · 🇮🇹 Italiano

WordPress plugin that installs and updates plugins hosted on your own Forgejo or Gitea instances, one or more at the same time, with access tokens kept in wp-config.php.

Development, releases and issues are on Forgejo. The GitHub repository is a read-only mirror.


Project structure

eg-forgejo-updater/
├── eg-forgejo-updater.php            # Bootstrap, plugin header, class includes
├── uninstall.php                     # Data cleanup on uninstall
├── index.php                         # Directory listing protection
├── readme.txt                        # Standard WordPress readme
├── changelog.txt                     # Changelog of versions before 3.0.0
├── CHANGELOG.md                      # Development log (not in the package)
├── LICENSE                           # GPL-2.0-or-later
└── includes/
    ├── class-eg-forgejo-updater-api.php    # Forgejo REST API client and token map
    ├── class-eg-forgejo-updater.php        # WordPress hooks for update detection and install
    ├── class-eg-forgejo-updater-admin.php  # Admin page (Plugins submenu)
    └── index.php                           # Directory listing protection

Requirements

  • WordPress 6.0 or later
  • PHP 8.0 or later (uses typed properties and named arguments)
  • Forgejo or Gitea instances reachable over HTTPS from the WordPress host
  • For each managed plugin: a release in the corresponding Forgejo repo is recommended (without releases, updates come from the primary branch)

Installation

  1. Download eg-forgejo-updater.zip from the latest release and upload it from Dashboard > Plugins > Add New > Upload Plugin
  2. Add EG_FORGEJO_UPDATER_TOKENS to wp-config.php (required for private repos)
  3. Activate the plugin from Dashboard > Plugins
  4. Plugins with a Forgejo Plugin URI or Gitea Plugin URI header are detected automatically
  5. To install additional plugins from Forgejo, use the built-in feature described below

Use the release ZIP, not GitHub's "Download ZIP" button: that archive contains the latest commits instead of the latest release, in a folder named eg-forgejo-updater-main/.


Configuration in wp-config.php

Add one access token per instance before the line /* That's all, stop editing! */:

define( 'EG_FORGEJO_UPDATER_TOKENS', array(
    'https://git.example.com'  => 'token_for_this_instance',
    'https://code.example.org' => 'token_for_the_other_instance',
) );

Keys are the https address of each instance; path, case and trailing slash are ignored, a port other than 443 must be written (https://git.example.com:8443). Entries with an http:// key or an empty token are ignored and reported on the admin page. Public repositories need no entry.

How to create an access token

  1. Log in to your Forgejo instance with your account
  2. Go to Settings → Applications → Manage Access Tokens
  3. Create a new token with the following permissions:
    • Repository → Read (to read raw files and download ZIPs)
    • Everything else: No access
  4. Copy the generated token and paste it into wp-config.php

An administrator account is not required: the account that owns the repositories you need to manage is enough.

Where the tokens go

Each token is sent in the Authorization: token xxx header:

  1. of the plugin's own Forgejo REST API calls to that instance
  2. of the download of the package being installed or updated, when it comes from that instance

The match is exact on scheme, host and port: a token is never sent to another instance, not even when a plugin header points elsewhere. The http_request_args filter only attaches it to package URLs chosen by the updater, so requests made by other plugins to the same instance stay anonymous. When a token is attached, redirects are disabled, because WordPress would forward the Authorization header to the redirect target.

Upgrading from 2.x

FORGEJO_ACCESS_TOKEN is no longer read: move its value into EG_FORGEJO_UPDATER_TOKENS before updating, then remove it (the admin page reminds you while it is still defined). FORGEJO_BASE_URL and the short owner/repo header form are no longer used by this plugin; leave the constant in place if another plugin needs it.

Plugin auto-discovery

The plugin detects the plugins to manage by reading the header of the main PHP file. Two headers are supported; the value is always the full repository URL:

/**
 * Plugin Name: My Plugin
 * Forgejo Plugin URI: https://git.example.com/owner/my-plugin
 */

or:

/**
 * Plugin Name: My Plugin
 * Gitea Plugin URI: https://git.example.com/owner/my-plugin
 */

Plugins detected automatically cannot be removed from the list (just remove the header to exclude them); the tooltip on the Source column says whether a plugin was detected from its header or installed through the form.

Instances that cannot generate archives

The fallbacks (archive/{tag}.zip for a release without ZIP, archive/{branch}.zip for a repository without releases) depend on the instance generating archives. Some cannot: forgejo.it (Forgejo 15.0.3), for example, answers HTTP 500 to every /archive/ request. The version check still works, so the failure only shows up when the update is downloaded. On such an instance, attach the plugin ZIP to every release.

Deleting a plugin

The last column holds a trash icon for inactive plugins. It opens the standard WordPress confirmation page (plugins.php?action=delete-selected), which lists the files and runs the plugin's uninstall.php: its settings are removed too. Active plugins show a grey icon (deactivate first); EG Forgejo Updater itself and users without the delete_plugins capability get no icon.

Installation date

Under the installed version, the admin table shows when the plugin was installed or last updated on this site, in the site's date format (time in the tooltip). The date is the modification time of the plugin's main file, so it costs no request; it changes if the file is edited by hand, and a plugin copied with its original file dates (rsync -a, git clone) shows the date of the copy's source.

Plugins also listed on WordPress.org

When a managed plugin is published in the WordPress.org directory, the updater steps aside: the "View details" popup and the updates come from WordPress.org. In the admin table the Source column shows WordPress.org and the Repository column links to the plugin's WordPress.org page.

  • Detection reads the update_plugins transient filled by WordPress's own update check: an entry with id = w.org/plugins/<slug> means the plugin is in the directory. No extra request for plugins that are not.
  • Slug collisions: WordPress.org matches plugins by folder slug only. The updater steps aside only if the Contributors: line of the installed readme.txt shares at least one name with the plugin's contributors on WordPress.org (plugins/info/1.2, cached for one week). Otherwise the plugin stays on Forgejo and the Status column shows "Slug taken on WordPress.org".
  • Manual switch: the "Use Forgejo" button forces a directory plugin back to Forgejo (stored in eg_forgejo_updater_channels); "Use WordPress.org" undoes it.

Why in wp-config.php and not in the database

Tokens in wp-config.php stay out of database dumps and backups, cannot be read or changed from the WordPress admin, and do not depend on transients or the object cache.


How update detection works

Full flow

WordPress scheduler (every 12h) or manual action
  └── Filter site_transient_update_plugins
        └── For each plugin with a Forgejo/Gitea Plugin URI header:
              └── get_transient(eg_forgejo_updater_release_{md5(plugin_file)})
                    ├── Cache hit  → use cached data
                    └── Cache miss → call Forgejo API (2 calls)
                          └── GET /api/v1/repos/{owner}/{repo}/releases/latest
                                ├── Release found → version from tag_name, package = first attached .zip
                                │     └── GET …/raw/readme.txt?ref={tag}
                                ├── HTTP 404 (no releases) → fallback to the primary branch
                                │     ├── GET …/raw/{file}.php?ref={branch} → parse "Version:"
                                │     └── GET …/raw/readme.txt?ref={branch}
                                └── Other error (401/403, 5xx, network) → no fallback, shown in Status
                                      ├── Remote version > installed → inject into $transient->response
                                      └── Equal or lower version       → remove from $transient->response

Check frequency

The version-data cache is saved as a WordPress transient with a TTL of 12 hours (CACHE_TTL = 43200). This means:

  • The automatic check happens every 12 hours, in sync with the native WordPress cycle
  • Going to Dashboard > Updates and clicking "Check again" forces an immediate check
  • The "Check for updates now" button on the plugin page explicitly flushes all transients

Version comparison

WordPress internally uses version_compare() (PHP native). The remote version is the tag of the latest release without the leading v (v2.13.0 → 2.13.0). Only for repositories without releases is it read from the Version: header of the main PHP file on the primary branch.


ZIP file handling during the update

Archive download

The plugin downloads the first ZIP attached to the latest release:

https://git.example.com/owner/repo/releases/download/v2.13.0/repo.zip

If the release has no ZIP attached, it uses the tag archive (archive/v2.13.0.zip). For a repository without releases, the archive of the primary branch (archive/main.zip).

Every site receives the same files for a given version: commits pushed to the branch after a release are not distributed until the next release.

The folder-name problem

The folder inside the ZIP does not always match the plugin slug (e.g. eg-fediverso-box/): Forgejo archives use the repository name, and a ZIP attached to an older release may carry the name the repository had back then. WordPress expects the extracted folder to have the same name as the slug.

The plugin handles this via the upgrader_source_selection filter, which renames the extracted folder to the correct slug before WordPress finishes the installation.


Installing a plugin from Forgejo

EG Forgejo Updater lets you install any public plugin hosted on Forgejo directly from WordPress, without downloading ZIPs, without FTP, without manual upload.

How to do it

  1. Go to Plugins → Forgejo Updater
  2. In the "Install plugin from Forgejo" section, paste the repository URL
  3. Click "Verify and continue"
  4. The plugin runs an automatic checklist:
    • Valid URL format
    • Recognized Forgejo/Gitea instance
    • Reachable repository
    • Main PHP file found
    • Plugin Name and Version headers present
    • Forgejo Plugin URI header present (required for automatic updates)
  5. If all checks pass, a preview with name, version and author is shown
  6. Click "Install plugin" to proceed
  7. Activate the plugin from the plugin list

Once installed, the plugin is automatically detected by the auto-discovery feature and will receive updates like the others.

Adding via header (auto-discovery)

For plugins already installed with the header in the main PHP file:

 * Forgejo Plugin URI: https://git.emanuelegori.uno/emanuelegori/repotest
 * Primary Branch: main

The plugin is detected automatically when the page is loaded, with no additional configuration.


Forgejo icon in the plugin list

In Plugins > Installed Plugins, every plugin managed by this updater shows an extra link in the meta info row (below the name and description), with the Forgejo logo and a direct link to the repository.

The SVG logo is embedded inline in PHP (get_forgejo_svg()), with no separate asset files and no additional HTTP requests.

In Dashboard > Updates, managed plugins show the icon images/icon.svg, served by your own site: no request leaves for WordPress.org or the repository.


WordPress hooks used

Hook Class Purpose
plugins_loaded bootstrap Class initialization
site_transient_update_plugins Updater Inject update data into the native WP cycle
plugins_api Updater "Version details" popup in the dashboard (left to WordPress for directory plugins)
plugin_row_meta Updater Forgejo icon and repo link in the plugin list
http_request_args Updater Add the instance token to package downloads chosen by the updater
upgrader_source_selection Updater Fix the ZIP folder name from zipball
upgrader_process_complete Updater Cache flush after a completed update
admin_menu Admin Register submenu under Plugins
admin_post_eg_forgejo_updater_preview Admin Verify repo URL and compatibility checklist
admin_post_eg_forgejo_updater_install Admin Install plugin from Forgejo zip
admin_post_eg_forgejo_updater_delete Admin Remove plugin from the list
admin_post_eg_forgejo_updater_check Admin Force an immediate update check
admin_post_eg_forgejo_updater_channel Admin Per-plugin channel: automatic or forced to Forgejo
admin_notices Admin Feedback notices
current_screen Admin Footer of the plugin page (admin_footer_text, update_footer)

Redis Object Cache compatibility

Transients are saved through native WordPress set_transient() / get_transient() / delete_transient() functions. With Redis Object Cache active they are automatically stored in Redis without extra configuration. No direct SQL queries are used.


Publishing a new version

For every new version of a managed plugin:

  1. Update Version: in the header of the plugin's main PHP file
  2. Commit and push to the primary branch
  3. Publish a release on Forgejo with tag v + version (e.g. v2.13.0), with the plugin ZIP attached. A CI workflow can do this on every push that changes the version.

Within 12 hours (or immediately by clicking "Check for updates now") WordPress will show the available-update notice. Without releases the plugin keeps updating from the branch.


Updating EG Forgejo Updater itself

This plugin manages itself thanks to the Forgejo Plugin URI header in its own main file. It updates exactly like the other plugins.


License

GPL-2.0-or-later