Handle workflows whose board or stack was deleted
Build package / php-lint (8.2) (push) Successful in 49s
Build package / php-lint (8.3) (push) Successful in 45s
Build package / php-lint (8.4) (push) Successful in 44s
Build package / xml-lint (push) Successful in 13s
Build package / unit-tests (push) Successful in 45s
Build package / package (push) Successful in 1m3s

Deleting a board or stack left the workflow row pointing at nothing: Deck
keeps the orphaned cards readable until its DeleteCron purges them, so the
run kept failing on the move every few minutes, silently and forever.

- The controller now rejects boards and stacks that do not exist (or are not
  the user's) on create and update, so no new broken row can be stored.
- The settings list marks affected workflows in red instead of showing a row
  that looks healthy.
- The background job disables such a workflow and mails its owner once. The
  mail ignores the notifyEmail flag: that one is about moved cards, this is a
  notice that the automation stopped. It stays a one-off because a disabled
  workflow is no longer picked up.

The check deliberately goes through StackService::findAll() rather than
BoardService::getUserBoards(): Deck injects the current user into BoardService
as a string frozen at construction, so in the job -- one process, many users,
a cached BoardService -- it would answer for the wrong user or for none, and
every workflow on the instance would have been disabled. findStackIds()
returns null only for a genuinely missing or forbidden board and throws for
anything else, so a Deck outage skips the workflow instead of killing it.
This commit is contained in:
Patrick Niebeling
2026-08-13 15:47:55 +02:00
parent ee72d6fcf9
commit 0eff367268
7 changed files with 279 additions and 5 deletions
+25 -3
View File
@@ -59,7 +59,7 @@ class WorkflowController extends OCSController {
bool $notifyEmail = false,
bool $enabled = true,
): DataResponse {
$this->validate($title, $sourceStackId, $targetStackId);
$this->validate($title, $boardId, $sourceStackId, $targetStackId);
$workflow = new Workflow();
$workflow->setUserId($this->currentUserId());
@@ -85,7 +85,7 @@ class WorkflowController extends OCSController {
bool $notifyEmail = false,
bool $enabled = true,
): DataResponse {
$this->validate($title, $sourceStackId, $targetStackId);
$this->validate($title, $boardId, $sourceStackId, $targetStackId);
try {
$workflow = $this->workflowMapper->findForUser($id, $this->currentUserId());
@@ -147,13 +147,35 @@ class WorkflowController extends OCSController {
}
}
private function validate(string $title, int $sourceStackId, int $targetStackId): void {
private function validate(string $title, int $boardId, int $sourceStackId, int $targetStackId): void {
if (trim($title) === '') {
throw new OCSBadRequestException('Title must not be empty');
}
if ($sourceStackId === $targetStackId) {
throw new OCSBadRequestException('Source and target stack must differ');
}
// Reject references to boards or stacks that are gone (or were never
// the user's) instead of storing a workflow that can only fail later.
// assertDeck() ran first, so an empty list here means "not there",
// not "Deck is unreachable".
$this->assertDeck();
$boardIds = array_map(
static fn (array $board) => (int)$board['id'],
$this->deckService->listBoardsForCurrentUser(),
);
if (!in_array($boardId, $boardIds, true)) {
throw new OCSBadRequestException('The selected board does not exist or is not accessible');
}
$stackIds = array_map(
static fn (array $stack) => (int)$stack['id'],
$this->deckService->listStacks($boardId),
);
if (!in_array($sourceStackId, $stackIds, true) || !in_array($targetStackId, $stackIds, true)) {
throw new OCSBadRequestException('The selected stack does not exist on this board');
}
}
/**