Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 3 additions & 29 deletions catalog.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,16 +95,15 @@
"_generated": "GENERATED BY tools/catalog — DO NOT HAND EDIT",
"advertised": 168,
"free": {
"entries": 131,
"entries": 130,
"installable": 124,
"nonInstallable": [
"family-ancestry",
"family-familysearch",
"family-gedcom",
"family-myheritage",
"family-wikitree",
"shared-utils",
"transactional-email"
"shared-utils"
]
},
"generated_from": {
Expand Down Expand Up @@ -133,7 +132,7 @@
},
"schema_version": 1,
"totals": {
"entries": 175,
"entries": 174,
"installable": 168
}
},
Expand Down Expand Up @@ -5347,31 +5346,6 @@
"tier_pair": false,
"version": "1.2.13"
},
{
"bundles": [],
"category": "communication",
"checksum": "c855f419baf8346687bc876bdd766a715adb141b8b55a096016247eb9176411b",
"commands": null,
"description": "Provider-agnostic transactional email: template rendering, per-tenant domain management, SPF/DKIM reporting, delivery webhook relay.",
"docs_url": null,
"installable": false,
"maturity": "planned",
"release_signature": {
"alg": "ed25519",
"key_id": "plugins-2ebca8dfad98e19e",
"sig": "KDunM6V7oR5WWdxCGKhI5LRaGipqdPkH0JfEb8aZcwRArtujni/qHso0/4mNyFml5egj3IndMJu8K4S7EFdTCw=="
},
"requires": {
"nself": ">=1.1.0"
},
"schema": "np_transactional_email",
"service_kind": "library",
"slug": "transactional-email",
"tarball_url": "https://github.com/nself-org/plugins/releases/download/v1.2.13/transactional-email-1.2.13.tar.gz",
"tier": "free",
"tier_pair": false,
"version": "1.2.13"
},
{
"bundles": [
"claw"
Expand Down
7 changes: 3 additions & 4 deletions counts.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,15 @@
"_generated": "GENERATED BY tools/catalog — DO NOT HAND EDIT",
"advertised": 168,
"free": {
"entries": 131,
"entries": 130,
"installable": 124,
"nonInstallable": [
"family-ancestry",
"family-familysearch",
"family-gedcom",
"family-myheritage",
"family-wikitree",
"shared-utils",
"transactional-email"
"shared-utils"
]
},
"generated_from": {
Expand Down Expand Up @@ -40,7 +39,7 @@
},
"schema_version": 1,
"totals": {
"entries": 175,
"entries": 174,
"installable": 168
}
}
56 changes: 1 addition & 55 deletions registry.json
Original file line number Diff line number Diff line change
Expand Up @@ -7579,60 +7579,6 @@
"tier": "free",
"version": "1.2.13"
},
"transactional-email": {
"author": "nself",
"catalog": {
"maturity": "planned",
"requires": {
"nself": ">=1.1.0"
},
"schema": "np_transactional_email",
"service_kind": "library"
},
"category": "communication",
"checksum": "c855f419baf8346687bc876bdd766a715adb141b8b55a096016247eb9176411b",
"checksums": {
"sha256": "c855f419baf8346687bc876bdd766a715adb141b8b55a096016247eb9176411b"
},
"dependencies": [],
"description": "Provider-agnostic transactional email: template rendering, per-tenant domain management, SPF/DKIM reporting, delivery webhook relay.",
"download_url": "https://plugins.nself.org/plugins/transactional-email/tarball",
"implementation": {
"entryPoint": "binary",
"language": "go"
},
"installable": false,
"language": "go",
"license": "MIT",
"minNselfVersion": "1.1.0",
"min_nself_version": "1.1.0",
"name": "transactional-email",
"releaseTag": "v1.2.13",
"release_signature": {
"alg": "ed25519",
"key_id": "plugins-2ebca8dfad98e19e",
"sig": "KDunM6V7oR5WWdxCGKhI5LRaGipqdPkH0JfEb8aZcwRArtujni/qHso0/4mNyFml5egj3IndMJu8K4S7EFdTCw=="
},
"requires_license": false,
"tables": [
"np_temail_domains",
"np_temail_messages",
"np_temail_templates"
],
"tags": [
"email",
"transactional",
"smtp",
"elastic-email",
"postmark",
"templates",
"webhooks"
],
"tarball": "https://github.com/nself-org/plugins/releases/download/v1.2.13/transactional-email-1.2.13.tar.gz",
"tarballUrl": "https://github.com/nself-org/plugins/releases/download/v1.2.13/transactional-email-1.2.13.tar.gz",
"tier": "free",
"version": "1.2.13"
},
"vpn": {
"author": "nself",
"catalog": {
Expand Down Expand Up @@ -8066,7 +8012,7 @@
"version": "1.2.13"
}
},
"plugins_count": 131,
"plugins_count": 130,
"schema_version": "2.0.0",
"tier": "free"
}
11 changes: 0 additions & 11 deletions releases.json
Original file line number Diff line number Diff line change
Expand Up @@ -2588,17 +2588,6 @@
"tarball_url": "https://github.com/nself-org/plugins/releases/download/v1.2.13/traefik-1.2.13.tar.gz",
"version": "1.2.13"
},
"transactional-email": {
"release_signature": {
"alg": "ed25519",
"key_id": "plugins-2ebca8dfad98e19e",
"sig": "KDunM6V7oR5WWdxCGKhI5LRaGipqdPkH0JfEb8aZcwRArtujni/qHso0/4mNyFml5egj3IndMJu8K4S7EFdTCw=="
},
"release_tag": "v1.2.13",
"sha256": "c855f419baf8346687bc876bdd766a715adb141b8b55a096016247eb9176411b",
"tarball_url": "https://github.com/nself-org/plugins/releases/download/v1.2.13/transactional-email-1.2.13.tar.gz",
"version": "1.2.13"
},
"vpn": {
"release_signature": {
"alg": "ed25519",
Expand Down
103 changes: 103 additions & 0 deletions scripts/ci/tests/test_registry_empty_plugins.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
"""Gate: the registry never serves a plugin that has no implementation (D-1458).

A registry entry with a tarball URL is installable bytes, signed as if shipped. A plugin whose free/<slug>
directory holds only its manifest and docs (plugin.json, README.md, SPEC.md ...) has no handlers, migrations,
compose fragment or source: `nself add <slug>` would install a plugin that does nothing. Rules:

* a registry entry that carries a tarball for an empty plugin fails, unless it is listed in EMPTY_SERVED;
* a listed slug that gained an implementation, or left the registry, fails until its line is deleted, so
the list only shrinks (same ratchet shape as test_plugin_nginx_server_ratchet.py).

EMPTY_SERVED is debt, not approval. The fix for a slug is to drop its row from releases.json and regenerate
(scripts/plugin-counts.sh), as was done for transactional-email.

Run: python3 -m unittest discover -s scripts/ci/tests -p 'test_registry_empty_plugins.py'
No docker, network or third-party packages.
"""
import json
import os
import unittest

ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", ".."))

# Files that describe a plugin without implementing it.
DOC_FILES = {"plugin.json", "README.md", "SPEC.md", "LICENSE", "CHANGELOG.md"}

# Still served as installable bytes with nothing behind them. Remove a line when the plugin is implemented
# or its release row is dropped.
EMPTY_SERVED = {
"family-familysearch",
"family-myheritage",
"family-wikitree",
"nself-geo",
"nself-image",
"nself-pdf",
"nself-scan",
}


def is_empty(slug):
"""True when free/<slug> is missing or holds nothing but manifest and docs."""
base = os.path.join(ROOT, "free", slug)
if not os.path.isdir(base):
return True
for _, _, files in os.walk(base):
if any(f not in DOC_FILES for f in files):
return False
return True


def served_empty(registry):
"""Slugs whose registry entry carries a tarball while the plugin directory is empty."""
plugins = registry["plugins"]
return sorted(
slug for slug, entry in plugins.items()
if (entry.get("tarball") or entry.get("download_url")) and is_empty(slug)
)


class RegistryEmptyPlugins(unittest.TestCase):
def setUp(self):
with open(os.path.join(ROOT, "registry.json")) as f:
self.registry = json.load(f)

def test_no_new_empty_plugin_is_served(self):
new = [s for s in served_empty(self.registry) if s not in EMPTY_SERVED]
self.assertEqual(
new, [],
"registry.json serves a tarball for a plugin with no implementation; drop its row from "
"releases.json and run scripts/plugin-counts.sh: %s" % ", ".join(new))

def test_empty_served_list_only_shrinks(self):
still = set(served_empty(self.registry))
stale = sorted(EMPTY_SERVED - still)
self.assertEqual(
stale, [],
"delete these from EMPTY_SERVED, they are implemented or no longer served: %s" % ", ".join(stale))

def test_detector_flags_a_docs_only_directory(self):
# The gate must be able to fail: a docs-only plugin dir is empty, one with a source file is not.
import tempfile
global ROOT
saved = ROOT
try:
with tempfile.TemporaryDirectory() as tmp:
ROOT = tmp
os.makedirs(os.path.join(tmp, "free", "docs-only"))
os.makedirs(os.path.join(tmp, "free", "real", "migrations"))
for name in ("plugin.json", "README.md", "SPEC.md"):
open(os.path.join(tmp, "free", "docs-only", name), "w").close()
open(os.path.join(tmp, "free", "real", "plugin.json"), "w").close()
open(os.path.join(tmp, "free", "real", "migrations", "001_init.up.sql"), "w").close()
reg = {"plugins": {
"docs-only": {"tarball": "https://example.invalid/a.tar.gz"},
"real": {"tarball": "https://example.invalid/b.tar.gz"},
"hidden": {},
}}
self.assertEqual(served_empty(reg), ["docs-only"])
finally:
ROOT = saved


if __name__ == "__main__":
unittest.main()
Loading