Files
Patrick Niebeling 9e1a15bab0
Build package / xml-lint (push) Successful in 1m4s
Build package / php-lint (8.5) (push) Successful in 4m0s
Build package / unit-tests (push) Successful in 4m13s
Build package / php-lint (8.4) (push) Successful in 4m34s
Build package / package (push) Successful in 1m38s
Refuse workflows that move cards in a loop
Every enabled workflow is an edge "source stack -> target stack";
LoopDetector finds circles in that graph, a workflow whose source is its
own target being the shortest. Without a check, two workflows A -> B and
B -> A move the same cards back and forth on every run, even within a
single run, with a notice for each move. The trigger date is ignored on
purpose: by start date one way and by due date the other, a card with
both dates passed still circles. Chains stay allowed.

The controller rejects saving an enabled workflow that would close a loop
and names the workflows involved. Workflows stored before this check are
skipped by the runner (logged, not disabled) and flagged in the settings
list via a new `loop` field from index().
2026-10-01 13:47:32 +02:00

7.9 KiB

Entwicklung

Informationen für Entwickler und Admins, die tiefer einsteigen. Die Nutzer-Doku steht in der README. Ausführliche Hintergründe zu den Stolperfallen, also warum etwas genau so gebaut ist, stehen in CLAUDE.md.

Aufbau

  • Keine HTTP/OCS-Aufrufe gegen Deck. Lesen (Boards, Stapel, Karten, Board-Mitglieder) und das Verschieben laufen ausschließlich über Decks interne PHP-Klassen im selben Prozess. Sämtlicher Deck-Zugriff ist in lib/Service/DeckIntegrationService.php gebündelt.
    • OCA\Deck\* ist keine stabile öffentliche API. Jeder Zugriff ist deshalb abgesichert: class_exists()-Checks, Prüfung ob Deck aktiviert ist, try/catch mit Logging.
    • Nach größeren Deck-Updates lohnt sich ein Blick ins Nextcloud-Log.
  • Hintergrundjob: RunWorkflowsJob ist ein einfacher Job ohne Intervall. Er läuft bei jedem Cron-Durchlauf und ruft WorkflowRunner auf.
  • Rechte-Kontext: Der Runner arbeitet die Workflows nach Eigentümer gruppiert ab und setzt Deck jeweils auf diesen Nutzer, per DeckIntegrationService::beginUserContext(). Decks Berechtigungsprüfungen lesen nämlich nicht die Session, sondern eine pro Prozess festgelegte Nutzer-ID.
  • E-Mail: NotificationMailer verschickt über Nextclouds IMailer. Die Empfänger ermittelt DeckIntegrationService::findNotificationRecipients(), die Entscheidungsregel steckt in NotificationPolicy.
  • Nextcloud-Benachrichtigungen: NotificationPusher legt sie über OCP\Notification\IManager an, eine pro Karte, an dieselben Empfänger wie die Mail. Er speichert nur Rohwerte. Den deutschen Text baut Notifier, registriert in Application::register(). Ist die App notifications für einen Empfänger nicht aktiv, wird nichts angelegt. Push aufs Handy übernimmt die Notifications-App selbst.
  • Oberfläche: src/PersonalSettings.vue (Vue 3, @nextcloud/vue 9) spricht über src/api.js mit dem OCS-Controller WorkflowController.

Datenmodell

Die Tabelle wfda_workflows enthält eine Zeile pro Workflow:

Spalte Bedeutung
board_id, source_stack_id, target_stack_id Deck-IDs
user_id Board-Eigentümer, in dessen Namen der Job läuft. Wird bei jedem Lauf aus Deck aktualisiert.
trigger_date due oder start: Fälligkeits- oder Startdatum löst das Verschieben aus
notify_mode off, owner oder board (wer)
notify_channel mail, notification oder both (wie)
enabled, last_run aktiv? / letzter Lauf

Migrationen in lib/Migration/:

  • Version1000… legt die Tabelle an.
  • Version1001… entfernt die früheren Filter-Spalten.
  • Version1002… und Version1003… ersetzen notify_email durch notify_mode. Ein gesetztes Häkchen wird dabei zu owner.
  • Version1004… fügt notify_channel hinzu. Bestehende Workflows bekommen mail.
  • Version1005… fügt trigger_date hinzu. Bestehende Workflows bekommen due.

Migrationen laufen nur, wenn die App-Version steigt.

Lokales Setup

Voraussetzungen: PHP 8.4+, Composer, Node.js 24 mit npm.

composer install
npm install          # nicht `npm ci`: Lockfiles werden bewusst nicht committet
npm run build        # Produktions-Build
npm run watch        # Entwicklung mit automatischem Rebuild

Tests und Linting

composer run lint        # php -l über lib/ und tests/
composer run cs:check    # nextcloud/coding-standard (cs:fix korrigiert)
composer run test:unit   # PHPUnit

Die Unit-Tests decken nur reine Logik ab, ohne Deck-Installation:

  • Datumsvergleich für Fälligkeits- und Startdatum (WorkflowRunner::hasPassed())
  • Auslöser und Begründungstext (TriggerDate)
  • Schleifenerkennung (LoopDetector)
  • Empfängerregel (NotificationPolicy::shouldNotify())
  • Benachrichtigungsweg (NotificationPolicy::sendsMail()/sendsNotification())

Die Deck-Anbindung lässt sich nur auf einer echten Nextcloud-Instanz mit Deck testen:

# Job-ID und letzten Lauf anzeigen
php occ background-job:list --class 'OCA\WorkflowDeckAutomation\BackgroundJob\RunWorkflowsJob'

# Sofort ausführen, ohne auf Cron zu warten
php occ background-job:execute <id> --force-execute

Build, CI und Release

make appstore erzeugt build/artifacts/appstore/workflow_deck_automation.tar.gz.

Gebaut und veröffentlicht wird in .gitea/workflows/build-main.yml:

Job Zweck
php-lint php -l mit PHP 8.4 und 8.5
xml-lint prüft appinfo/info.xml gegen das Appstore-Schema
unit-tests PHPUnit
package Frontend-Build und make appstore. Läuft nur, wenn die Checks grün sind.

Das Paket wird als Release veröffentlicht:

  • Bei jedem Push auf main als Pre-Release latest-main, das jedes Mal neu erzeugt wird.
  • Bei einem v*-Tag als reguläres Release.
  • Bei Pull Requests wird nur gebaut, nicht veröffentlicht.

Versionen folgen dem Schema <Nextcloud-Major>.<minor>.<patch>. Die unterstützte Nextcloud-Version steht in appinfo/info.xml.

Ablauf für ein Release:

  1. Version in appinfo/info.xml und package.json gleich anheben.
  2. Auf main pushen und warten, bis die Pipeline grün ist.
  3. git tag -a vX.Y.Z -m "…" && git push origin vX.Y.Z.
  4. Optional in den App Store: in Gitea unter Actions den Workflow „Publish to App Store“ von Hand starten und den Tag angeben.

App Store

.gitea/workflows/appstore.yml läuft nur von Hand (workflow_dispatch). Er baut nichts selbst. Er lädt den Tarball eines vorhandenen vX.Y.Z-Release ohne Anmeldung herunter und prüft, ob die Version in info.xml zum Tag passt. Dann signiert er das Archiv (SHA-512) und meldet die Download-URL samt Signatur an die App-Store-API. Der Store lädt den Tarball danach selbst von Gitea. Die Releases müssen deshalb öffentlich erreichbar sein, und ein Tag darf nach dem Store-Upload nicht mehr verschoben werden.

Benötigte Repository-Secrets:

Secret Inhalt
APPSTORE_PRIVATE_KEY privater Schlüssel (PEM), zu dem Nextcloud das App-Zertifikat ausgestellt hat
APPSTORE_TOKEN API-Token von https://apps.nextcloud.com/account/token

Einmalig vorher: Zertifikat über nextcloud/app-certificate-requests beantragen und die App auf apps.nextcloud.com registrieren. Die App wird nicht per Code-Signing (appinfo/signature.json) signiert. Nextcloud prüft Apps, die nicht zum Core gehören, nur dann, wenn diese Datei vorhanden ist.

Einzelheiten, etwa zum Verschieben eines Tags nach einem fehlgeschlagenen Build, stehen in CLAUDE.md.

Fehlersuche: Der Job läuft nur nachts oder gar nicht

Nextcloud überspringt „nicht zeitkritische“ Jobs außerhalb des Wartungsfensters (maintenance_window_start in der config.php). Bis einschließlich v0.1.0 war dieser Job als nicht zeitkritisch markiert. Diese Markierung bleibt in der Spalte oc_jobs.time_sensitive stehen, auch nach App-Updates.

Symptom: Der Job läuft nur im 4-Stunden-Wartungsfenster, oder occ background-job:list zeigt als letzten Lauf 1970-01-01.

Einmalig korrigieren:

UPDATE oc_jobs SET time_sensitive = 0
 WHERE class = 'OCA\\WorkflowDeckAutomation\\BackgroundJob\\RunWorkflowsJob';

Oder ohne Datenbankzugriff den Job löschen und durch Aus- und Einschalten der App neu registrieren:

php occ background-job:delete <id>
php occ app:disable workflow_deck_automation
php occ app:enable workflow_deck_automation

Die frühere config.php-Option workflow_deck_automation.interval (bis v0.1.0) wird ignoriert und kann entfernt werden.