Was ein Plugin kann
Ein Plugin ist ein Python-Paket mit einem kleinen React-Frontend. Es bringt eines oder mehrere Module mit, genau wie die eingebauten Module von Hubra:
- Backend: API-Routen mit Berechtigungen, eigene Tabellen in jedem Verein, Einstellungen, Einträge in der Navigation, Event-Hooks und Celery-Aufgaben.
- Frontend: eigene Seiten, Tabs auf eingebauten Seiten und Karten an festen Stellen (Slots) der Vereins-App, mit Übersetzungen in Deutsch und Englisch.
- Versionen: jede Version ist signiert, hat ihre Änderungen dabei und kann zurückgerollt werden.
Plattform-Admins installieren ein Plugin einmal aus dem Marktplatz, danach schaltet jeder Verein seine Module unter Einstellungen › Module & Plugins ein oder aus.
Ein Plugin läuft mit vollem Zugriff auf die Daten aller Vereine einer Hubra. Darum installiert eine Hubra nur signierte Plugins, und auf hubra.club prüfen wir jedes Plugin, bevor wir es installieren.
Mit der Vorlage starten
Die Vorlage hubra-plugins-template ist ein kleines, funktionierendes Plugin (eine Pinnwand) mit allen Teilen: Tabelle, API, Bildschirm, Einstellung, Migration, Test und der Workflow, der testet, signiert und veröffentlicht.
- Auf GitHub Use this template wählen und das Repository
hubra-plugin-<id>nennen. - Alles umbenennen, was
exampleheisst: Plugin-ID, Paket (hubra_<name>), Modul-Code, Tabellen, API-Pfad, Route und Übersetzungen. Die Tabelle dazu steht im README der Vorlage. - Die Beispiel-Migration durch eine eigene ersetzen,
version: "1.0.0"setzen und den ersten Eintrag inCHANGELOG.mdschreiben. - Im Repository die Rulesets aus
.github/rulesets/importieren und Secret Scanning einschalten.
Aufbau
plugin.yaml # ID, Version, Hubra-Versionen, Paket, Module
CHANGELOG.md # "## 1.1.0"-Abschnitte, vor jedem Update angezeigt
.releaseignore # bleibt aus dem Release-Zip draussen
hubra_example/
__init__.py # module = Module(...)
models.py # SQLAlchemy-Modelle (eine Kopie pro Verein)
routes.py # die API, mit permission_required(...)
migrations/ # Alembic-Revisionen nur dieses Plugins
frontend/src/
index.tsx # registerPlugin({...})
translations.json # en-GB und de-CH
tests/ # läuft mit den pytest-Fixtures von Hubra
.github/workflows/release.yml
Die Datei plugin.yaml beschreibt das Plugin. Name, Beschreibung und Tags erscheinen so im Marktplatz:
id: example
name: Example
version: "1.0.0"
hubra: ">=0.2,<1" # Hubra-Versionen, mit denen es läuft
description: "Ein Satz: was es tut"
author: Hubra
homepage: https://github.com/hubra-app/hubra-plugin-example
license: AGPL-3.0-or-later
tags: [inventory]
package: hubra_example # muss mit hubra_ beginnen
modules: [example]
requires: [members] # Module oder Plugins, die es braucht
migrations: migrations
frontend: frontend/dist/plugin.js
Backend
Das Paket definiert Module-Objekte wie die eingebauten Module: Blueprints, Navigation, Einstellungen, Berechtigungen und Tabellen. Modul-Codes dürfen nicht mit denen von Hubra kollidieren, Tabellen beginnen mit der Plugin-ID.
from hubra.core.modules import Blueprint, Module, NavItem, SettingField
module = Module(
code="example",
name="Example",
description="A notice board: short notes for the whole club",
default_enabled=False,
depends_on=("members",),
blueprints=(Blueprint("hubra_example.routes", "example_bp", "/api/v1/example"),),
nav=(NavItem("/app/example", "shell.nav.example", "note",
section="more", perms=("example.notes.read",)),),
settings=(SettingField("max_notes", "int", default=50,
label="Most notes on the board"),),
tables=("example_notes",),
)
Der Name des Blueprints legt den Präfix der Berechtigungen fest: example_bp ergibt example.notes.read. Migrationen schreibt ihr mit flask plugins revision; sie laufen in jedem Verein, mit einer eigenen Versionstabelle pro Plugin.
Frontend
frontend/src/index.tsx meldet an, was das Plugin in der Vereins-App zeigt. Alles erscheint nur, solange das Modul im Verein eingeschaltet ist und die Person die Berechtigung hat.
import { registerPlugin } from '@hubra/plugin-sdk'
import { IconNote } from '@tabler/icons-react'
import { NotesScreen } from './NotesScreen'
import translations from './translations.json'
registerPlugin({
id: 'example',
routes: [{ path: 'example', module: 'example', perm: 'example.notes.read', component: NotesScreen }],
icons: { note: IconNote },
translations, // { "en-GB": {...}, "de-CH": {...} }
})
Weitere Stellen: tabs (ein Tab auf einer eingebauten Seite) und slots (account.cards, inventory.item, settings.module.actions). React, Mantine, react-query, react-router und react-i18next kommen zur Laufzeit aus der App; alles andere, etwa Icons, wird mitgebündelt.
Lokal entwickeln und testen
Klont das Plugin nach plugins/<id> eines Hubra-Checkouts. Mit HUBRA_PLUGINS_AUTOINSTALL=<id> lädt eine lokale Hubra das Plugin direkt aus dem Ordner, ohne Installation.
cd hubra/app/backend
pytest ../../plugins/<id> # Tests mit den Fixtures von Hubra
flask plugins revision <id> -m "..." --autogenerate # neue Migration
cd ../frontend
npm run typecheck:plugins
npm run build:plugin -- ../../plugins/<id> # --watch beim Arbeiten
Der Workflow im Plugin-Repository macht dasselbe bei jedem Push, gegen den aktuellen main von Hubra.
Veröffentlichen
Im offiziellen Marktplatz
Die offiziellen Plugins leben je in einem öffentlichen Repository hubra-app/hubra-plugin-<id>. Für ein Release:
versioninplugin.yamlerhöhen und einen Abschnitt## <version>inCHANGELOG.mdschreiben.- Mergen und ein GitHub-Release mit dem Tag
v<version>veröffentlichen. - Der Workflow testet, baut das Frontend, signiert nach Freigabe mit dem Hubra-Pluginschlüssel und hängt
<id>-<version>.zipmit Prüfsumme an. - Dieser Marktplatz findet das Release innert einer Stunde, prüft Signatur und Prüfsumme, signiert den Index neu und zeigt die Version hier an.
Ihr habt ein Plugin gebaut, das in den offiziellen Marktplatz soll? Schreibt uns an [email protected].
In einem eigenen Marktplatz
Jede Hubra kann weitere Repositories hinzufügen. Mit eurem eigenen Schlüssel signiert ihr Plugins und Index selbst und stellt index.json auf einen beliebigen Webserver, zum Beispiel GitHub Pages:
flask plugins keygen index.pem # einmal: Indexschlüssel
flask plugins keygen plugins.pem # einmal: Pluginschlüssel
flask plugins repo ./plugins ./public --key index.pem --plugin-key plugins.pem --name "Meine Plugins"
flask plugins check https://ich.github.io/plugins/index.json --key-id <key id>
Plattform-Admins tragen dann die URL und die Schlüssel-ID im Plugin-Katalog › Quellen ein. Eure Plugins erscheinen dort als «Community».
Regeln, die gelten
- Migrationen bleiben rückwärtskompatibel. Ein Rollback setzt die Version zurück, nicht die Datenbank. Spalten und Tabellen also hinzufügen und erst ein Release später entfernen.
- Eigene Namen. Paket mit
hubra_, Tabellen mit der Plugin-ID als Präfix, Modul-Codes ohne Kollision mit Hubra. - Ein Schlüssel, ein Zweck. Pluginschlüssel signieren Plugins, Indexschlüssel nur den Index. Private Schlüssel gehören in ein GitHub-Environment, nie in ein Repository- oder Organisations-Secret.
- Release-Tags sind endgültig. Eine fehlerhafte Version wird nicht ersetzt, sondern zurückgezogen und durch eine neue Version abgelöst.
- Sicherheitslücken meldet ihr vertraulich über die Security-Seite des jeweiligen Repositories.
Weiterlesen
- plugins/README.md: alles zu Plugins, für Admins und Autoren (Englisch).
- hubra/modules/README.md: wie Module funktionieren.
- hubra-plugin-keys: ein grösseres, echtes Plugin zum Vergleichen.
- hubra-plugins: wie dieser Marktplatz gebaut und signiert wird.