This document is the step-by-step submission and review process for
nginxui/plugins. Read README.md first for the catalog's shape and the
trust levels referenced below.
Before you submit, your plugin should have:
- A public GitHub repository with at least one GitHub Release whose tag
looks like
v1.0.0(SemVer) and whose assets include the packaged.tar.gzyour plugin builds: either the portable<id>-<version>.tar.gz, or one<id>-<version>-<goos>-<goarch>.tar.gzper platform, or both (see Packaging for the archive layout). - A
plugin.jsonthat passesnginx-ui plugin lintand, for a plugin with aserverblock,nginx-ui plugin conformance— the same two commands.github/workflows/validate.ymlruns against your release. - A plugin
idthat follows the naming rule inREADME.md(io.github.<owner>.<name>if you have no domain of your own).idmust never start withcom.nginxui., which is reserved for plugins the nginx-ui project maintains itself. - A primary key and a signing key it certified, made once with
nginx-ui plugin key init --id <your plugin id>. Every package carriesplugin.sums, its signatureplugin.sums.minisigby the signing key and the certificateplugin.signerandplugin.signer.minisigat its root;nginx-ui plugin pack --key signing.keywrites all of them, seedocs/signing.md. A package without a signature is unsigned and installs only on a host in developer mode.
Open a new Issue and pick Submit a plugin
(.github/ISSUE_TEMPLATE/submit-plugin.yml). Fill in your repository URL,
your primary public key and, if you like, the categories. The issue is the
whole submission, there is no pull request to follow:
.github/workflows/submit-issue.ymldraftsplugins/<id>.jsonfrom your newest release (scripts/submission/): the id and the names come from itsplugin.json, the license from your repository, the categories from your choice or the capabilities of the plugin. The names are reviewed in every language, and none may claim to be official.- It checks the draft the way a pull request is checked, see
What
.github/workflows/validate.ymlchecks, and posts a status comment with the result, what the listing will show and the drafted entry. The status comment is updated in place, so it always describes the last checked version of the issue. - To change something, edit the issue. Your edits run the checks again, so do a new release when the problem was in the packages.
- A maintainer reviews the issue, see Review and merge,
and adds the
approvedlabel. The checks run once more and the entry is committed tomain, credited to you. The deploy then comments the plugin page, labels the issuelistedand closes it. An edit after the approval withdraws it. From then on the issue follows your listing: it opens again when a release names your plugin in a new way, see What your releases change. Approving, listing and a withdrawn approval each add a short comment, so the people concerned are notified.
The repository has to agree to be listed. The submission checks that you own
it, that you are a public member of the organization that owns it, or that it
has the nginx-ui-plugin topic, which only its administrators can set.
Otherwise a maintainer confirms it with the owners before approving.
Fork this repository and add plugins/<id>.json yourself — copy the shape of
an existing entry such as plugins/com.nginxui.dns01.json, or validate
against schema/entry.schema.json directly. Open a pull request. The entry
lists no releases: they are read from your GitHub Releases (see below).
Required fields and what they mean are documented in
schema/entry.schema.json; in particular:
nameis a locale map of the reviewed names,{ "en": "...", "zh_CN": "..." }. Everything else the listing shows comes from theplugin.jsonof your newest stable release, see What your releases change: leavedescription,homepage_url,readme_url,icon_urlandscreenshotsout unless you need to override it.trustfor a submission is always"community".verifiedis reserved for partner organizations, see Partner plugins.repository_urlis the GitHub repository whose Releases publish the plugin. Each release carries its packages, the portable<id>-<version>.tar.gzor one<id>-<version>-<goos>-<goarch>.tar.gzper platform with a.sha256file next to each, see Packaging. The signature lives inside each package.author_public_keyis required for acommunityentry. It is your primary public key,primary.pubfromnginx-ui plugin key init, which certifies the signing keys of your packages.revoked_signerslists the ids of signing keys you withdrew, see Withdrawing a signing key.categoriesis optional: one to three ids from thecategorylist ofschema/entry.schema.json, such ascertificatesorlogs. The marketplace and the catalog site translate them and filter by them.screenshotsoverrides the screenshots of yourplugin.json: up to eight images, each anhttpsurlof a PNG, JPEG or WebP file with an optionaldark_urlandcaptionlocale map. NGINX UI shows only images served from your repository's GitHub host, the catalog or the host of your packages.
On every pull request touching plugins/*.json, for each changed or added
entry:
- Schema:
node scripts/validate.mjs. The entry matchesschema/entry.schema.json, the file is named<id>.json, the id is unique catalog-wide,repository_urlis a GitHub repository (for anio.github.<owner>.<name>id, owned by<owner>), and acommunityentry has anauthor_public_key. - Releases:
scripts/build-catalog.mjsbuilds the entry from your GitHub Releases. For every release the published catalog does not list yet, and for the newest one, it downloads every package, checks it against the sha256 GitHub records for the asset and extracts it. Bothplugin.sumsandplugin.sums.minisigmust sit at the package root, every regular file besides them must be listed inplugin.sumswith a matching digest, andplugin.sums.minisigmust verify against the key of the entry's trust level: for acommunityentry a signing key that the signer certificate in the package names, that yourauthor_public_keycertified for this plugin and thatrevoked_signersdoes not list; the partner certificate orpartners/for averifiedone (see Partner plugins); the official plugin key for anofficialone. The job summary lists what the entry will show. - Lint and conformance: runs
nginx-ui plugin lintandnginx-ui plugin conformanceagainst the package a linux-amd64 host would install (downloads["linux-amd64"],downloads["any"], or the portable package, in that order) inside theuozicoder/nginx-uicontainer image.
A release that does not verify is left out of the catalog, and the pull request is not mergeable until the newest release verifies.
A maintainer reviews the submission once, with the checks and the listing preview in front of them:
- The repository agrees to be listed: the submission check passed, or the owners confirmed it.
- The name does not pass the plugin off as another one or as the Nginx UI project's.
- The permissions and capabilities fit what the plugin does, and
permission_reasonsexplain them. - The README and the screenshots are about the plugin and follow the code of conduct.
- The license and the categories fit.
An issue is approved with the approved label, a pull request is merged.
.github/workflows/deploy.yml then builds the catalog and deploys it, and
your plugin is live at the default catalog URL within the cache time of
nginx-ui hosts (internal/plugin.Marketplace caches a source for up to one
hour, or refreshes at once for a user who hits "refresh"). Later releases
are not reviewed one by one: the deploy lists them, and the maintainers look
over the listing changes it reports.
Nothing to do: the deploy reads your GitHub Releases every hour and lists a new one as soon as its packages verify. Prereleases are listed too, on the channel their version names, see the channel rules in the README. A draft release is ignored until it is published.
The listing follows the release it is shown with, the newest stable release that is not yanked, or the newest release while there is no stable one:
| Listing | From that release |
|---|---|
| Name | name and i18n.<language>.name of plugin.json, once a maintainer approved it, see below. |
| Description | description and i18n.<language>.description of plugin.json, unless it claims to be official, see below. |
| Homepage | homepage_url of plugin.json |
| README | README.md at the tag of the release |
| Icon | the file icon_path of plugin.json names in the package, PNG, WebP or SVG of at most 256 KB, which the catalog serves |
| Screenshots | screenshots of plugin.json, read from your repository at the tag, see Manifest |
A name can claim to be official in any language, so every name is reviewed, English included. The listing shows the names of your entry. When a release names your plugin in a way your entry does not hold, in any language, the deploy reopens the issue of your plugin (your submission issue, or one it opens) with a checkbox per name. A maintainer ticks the names to list, which adds them to your entry, or declines the rest; until then the listing keeps the names it had. A name holding a word such as "official" or "官方", or a character that does not show, is left out.
Descriptions are not reviewed. One that names Nginx UI and claims to be official in the same sentence, such as "the official Nginx UI DNS plugin", or holds a character that does not show, is left out, the listing keeps the description it had, and the issue of your plugin says so. "The official Cloudflare API" is fine.
The official plugins of the Nginx UI project take their names and descriptions from their releases directly.
A field your entry sets wins: per language for the description, as a whole for the others. Maintainers use this to correct a listing. The deploy lists every entry whose listing changed in its job summary, and the maintainers look over the changes after the fact.
To replace a signing key, run nginx-ui plugin key rotate and sign your next
release with the new one, nothing changes here. When a signing key may have
leaked, open a pull request that adds its id to revoked_signers of your
entry; nginx-ui plugin key revoke signing.pub prints the line. Every
release that key signed is then yanked and hosts treat its packages as
unsigned, so publish a new release signed with a new key.
To have a release listed within minutes of publishing it instead, install the
NGINX UI Plugin Catalog
GitHub App: choose the account that owns the plugin repository, select only
that repository, and install. Publishing a release then starts the deploy of
the catalog at once. The App only receives release events and holds no
private key, so it cannot read your repository, and it does nothing for a
repository the catalog does not list yet. See
worker/README.md.
A listed release is pinned: replacing a package of it on GitHub fails the deploy instead of changing what hosts download. Publish a new version instead.
verified trust is reserved for partner organizations the maintainers
vouch for. It says who published a package and makes no claim that anyone
reviewed its source.
- The partner contacts the nginx-ui maintainers. Once agreed, it generates
a minisign key pair (
minisign -G -p partner.pub -s partner.key) and sends only its public key,partner.pub. - The maintainers add
partners/<name>.jsonwith that key, which lists it in the signed partner keyringv1/partners.json, and send back a partner certificate:plugin.partner(the partner's public key) andplugin.partner.minisig(an official plugin key signature over it that names the partner and, optionally, an expiry date). See "Issuing a partner certificate" indocs/signing.md. - The partner puts both files, unchanged, at the root of every package
before writing
plugin.sums, soplugin.sumslists them like any other file, and signsplugin.sumswith its own key as usual. - Its catalog entries use
"trust": "verified"and need noauthor_public_key.
Hosts install such a package as verified when the certificate verifies
against the official plugin key they pin, or when their copy of the keyring lists
the key. No nginx-ui release is involved. If the key may have leaked, tell
the maintainers through a private security advisory: they revoke it in
partners/, and hosts stop treating it as a partner key on their next
catalog refresh, certificate or not. Revocation is the main safeguard. The
maintainers still recommend a long expiry on the certificate, because a
host that never refreshes the keyring would otherwise trust a leaked key
forever. A certificate with an expiry stops working after that date, so ask
the maintainers for a new one before then.
Yanking withdraws a specific release, not the whole plugin: its version goes
into the yanked list of plugins/<id>.json, and the catalog keeps the
release marked "yanked": true. A yanked release:
- Is never offered as the
installable_releaseto a node that has not already installed it. - Is still reported to a node that already has it installed, as
installed_yanked: true, so the UI can prompt the user to update or remove it.
To yank a release:
- Open an Issue or a security advisory describing the problem (a vulnerability, a broken build, a credential leak, a license violation, etc.). Do not put exploit details in a public Issue, use a private security advisory instead for anything actively exploitable.
- A maintainer (or the plugin author, for their own plugin) opens a PR
adding the version to
"yanked". Do not delete the GitHub Release: a node that already installed it still needs to see it in the catalog to know it should update. - Merge fast-tracks past the normal release cadence for security issues; the deploy republishes the catalog on merge.
- If every listed release of a plugin ends up yanked, consider whether the
plugin should be removed from
plugins/entirely instead (a separate PR, discussed with maintainers first).
A yank does not require the author's consent when the issue is a security one; maintainers may yank unilaterally and notify the author afterward.