# Using this setup in another plugin Everything here was built to be lifted into the next WordPress plugin. Nothing under `ci/` knows this plugin's name except `ci/plugin.conf`. There is no shared repository and no reusable workflow to `uses:`. That is deliberate: these repositories live on two forges, and a cross-forge `workflow_call` resolves on GitHub and fails on Gitea. Copying six files and editing one is a smaller cost than a dependency that works in half the places it runs. ## The port **1. Copy these, unchanged:** ``` ci/ every script; plugin.conf is the only one you edit .github/workflows/ ci.yml, compat.yml, release.yml, auto-merge.yml .gitea/workflows/ ci.yml, compat.yml, release.yml, README.md .distignore ``` **2. Rewrite `ci/plugin.conf`** — five values, all commented in the file: ```sh PLUGIN_SLUG=your-plugin PLUGIN_MAIN=your-plugin.php TEXT_DOMAIN=your-plugin NEEDS_WOOCOMMERCE=yes # no, for a plugin that does not need it SITE_TESTS="tests/fixtures.php tests/smoke.php" ``` **3. Adjust `.distignore`** for that plugin's internal documents. Everything left in `wp-content/plugins/` can in principle be fetched over HTTP from a live shop, so notes and tooling stay out of the zip. **4. Write `tests/fixtures.php` and `tests/smoke.php`.** These are the only genuinely new work. Use this plugin's as the shape: `wc_edm_ok()` / `wc_edm_is()` accumulate failures so one run reports all of them, and the file exits non-zero at the end. They run under `wp eval-file`, so WordPress and WooCommerce are fully loaded and every API is the real one. **5. Copy `includes/class-updater.php`** and rename the class, the `TRANSIENT` constant, the plugin-file constant and the user-agent string. Then add to the plugin header: ``` Update URI: https://gitea.nolimit.dk/kw/your-plugin ``` and instantiate the updater **before** any "is WooCommerce active" gate — a shop with WooCommerce switched off for an afternoon must not quietly stop receiving fixes. Three things in that file are load-bearing and none of them look it: - 🔴 **`offer_update()` answers even when the installed version is already current.** It is tempting to return `false` there — there is no update, after all — and that one line is what silently removes the **Enable auto-updates** link from the Plugins screen. `WP_Plugins_List_Table` sets `update-supported` only for plugins it finds in the update transient's `response` **or** `no_update` list; a plugin that stays silent is in neither, and wp-admin says "Auto-updates are not available for this plugin." The plugin looks perfectly configured and can never auto-update. `tests/smoke.php` asserts the rendered table, not the intention. - **Set `icons`.** Without it Dashboard → Updates shows a generic grey plug. Point it at the shop's own copy (`PLUGIN_URL . 'assets/icon.svg'`), not the release host: one fewer thing that must be reachable for wp-admin to render, and it cannot go mixed-content on an HTTPS shop. - **`Update URI` also switches wordpress.org off** for this plugin, from WordPress 5.8. Remove the header and a .org plugin that happens to share the slug could be pushed onto a shop running yours. The answer is cached for six hours, which is right for the check WordPress runs twice a day and wrong for the **Check again** link — so `forget_on_force_check()` drops the cache on `force-check`. Without it, someone who has just published a release clicks that link, sees nothing, and concludes the whole mechanism is broken. **6. Make the Gitea repository public**, or point `Update URI` at a separate public releases-only repository. Every shop reads that feed without credentials; `ci/publish-release.sh` fails the release with this instruction if it cannot. ## What you get - `ci/static.sh` — syntax on the oldest and newest PHP, the direct-access guard on every shipped file, no debug leftovers, no short tags, text domain on every translation call, one version stated once across header, constant and readme, and a warning when the compatibility headers have gone stale. - `ci/site-test.sh` — a real WordPress + WooCommerce at any versions you name, including `nightly` and `beta`, failing on any deprecation from your own code. - `ci/build-zip.sh` — an installable zip, checked for the main file, for a single top-level directory named for the slug, and for leaked tooling. - `compat.yml` — the weekly run against the next WordPress and the next WooCommerce, and a pull request that bumps the compatibility headers once that run is green. - `release.yml` — tag, test, build, publish, then read the feed back with no credentials to prove a shop can download it. - `ci/update-smoke.sh` — install the previous version into a throwaway shop and let WordPress update it by itself. The only check that proves the whole chain. - Assertions that the **Enable auto-updates** link is actually offered, built from the real `WP_Plugins_List_Table`, and that the update offer carries an icon that exists in the release. - `ci/workflows-in-sync.sh` — fails when the two workflow roots drift, which is the only way to notice that a workflow stopped running on Gitea. ## Two guards worth understanding before you move them **`.gitea/workflows/` hides `.github/workflows/` completely.** Gitea uses the former if it exists and never merges the two. Adding one Gitea-only workflow switches off CI for the whole repository there, with no error and no run to click on — it looks exactly like a runner that has not picked the job up. Every workflow that must run on Gitea has to be in that directory. **The release reads its own feed anonymously afterwards.** A private repository and a release with no zip attached both produce a page that looks perfect to anyone logged in, and an update that no shop is ever offered. A check that sends a token is the false positive that hides exactly that. **A forge's own download URL is not the address your users can reach.** `browser_download_url` is built from the forge's configured root URL; `gitea.nolimit.dk` reports `http://57.129.89.117:25001/...` there. The updater rebuilds the address from the `Update URI` header and keeps only the path. This one hides particularly well: a CI job on the same network reaches that raw IP, so following the reported URL goes green and only a real client outside fails. ## Candidates already in the fleet `spool` and `subscriptions` are both WooCommerce plugins with some of this already: `spool` has an earlier version of the throwaway-site script, and `subscriptions` has the hygiene checks and a `.distignore`. Neither has the version matrix, the compatibility schedule or the updater. Porting them is steps 1–5 above.