hubra plugins

Marktplatz › Plugins entwickeln

Ein Plugin für Hubra bauen.

Ein Plugin bringt neue Funktionen in Hubra, ohne Hubra selbst zu ändern: eigene Tabellen, eine API, Bildschirme in der Vereins-App und Einstellungen. Diese Seite führt von der Vorlage bis zum signierten Release.

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.

  1. Auf GitHub Use this template wählen und das Repository hubra-plugin-<id> nennen.
  2. Alles umbenennen, was example heisst: Plugin-ID, Paket (hubra_<name>), Modul-Code, Tabellen, API-Pfad, Route und Übersetzungen. Die Tabelle dazu steht im README der Vorlage.
  3. Die Beispiel-Migration durch eine eigene ersetzen, version: "1.0.0" setzen und den ersten Eintrag in CHANGELOG.md schreiben.
  4. 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:

  1. version in plugin.yaml erhöhen und einen Abschnitt ## <version> in CHANGELOG.md schreiben.
  2. Mergen und ein GitHub-Release mit dem Tag v<version> veröffentlichen.
  3. Der Workflow testet, baut das Frontend, signiert nach Freigabe mit dem Hubra-Pluginschlüssel und hängt <id>-<version>.zip mit Prüfsumme an.
  4. 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

Zum Marktplatz