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
+59
View File
@@ -78,6 +78,65 @@ class NotificationMailer {
}
}
/**
* One-off notice that a workflow was switched off because the board or
* stack it points at is gone.
*
* @param WorkflowRunner::TARGET_* $brokenTarget
*/
public function sendWorkflowDisabledNotification(IUser $user, Workflow $workflow, string $brokenTarget): void {
$email = $user->getEMailAddress();
if ($email === null || $email === '') {
$this->logger->info('Cannot notify about disabled workflow ' . $workflow->getId() . ': user ' . $user->getUID() . ' has no email address', [
'app' => 'workflow_deck_automation',
]);
return;
}
$reason = match ($brokenTarget) {
WorkflowRunner::TARGET_SOURCE_STACK => $this->l10n->t('its source stack no longer exists'),
WorkflowRunner::TARGET_TARGET_STACK => $this->l10n->t('its target stack no longer exists'),
default => $this->l10n->t('its board no longer exists or is no longer available to you'),
};
try {
$settingsLink = $this->urlGenerator->getAbsoluteURL('/settings/user/workflow_deck_automation');
$template = $this->mailer->createEMailTemplate('workflow_deck_automation.WorkflowDisabled', [
'workflowTitle' => $workflow->getTitle(),
]);
$template->setSubject($this->l10n->t('Deck workflow deactivated: %s', [$workflow->getTitle()]));
$template->addHeader();
$template->addHeading($this->l10n->t('A workflow was deactivated'), false);
$template->addBodyText($this->l10n->t(
'The workflow "%1$s" was switched off automatically because %2$s. No cards are being moved by it any more.',
[$workflow->getTitle(), $reason],
));
$template->addBodyText($this->l10n->t(
'Delete the workflow or point it at an existing board and stack to reactivate it. You will not be reminded about this workflow again.',
));
$template->addBodyButton($this->l10n->t('Open settings'), $settingsLink);
$template->addFooter();
$message = $this->mailer->createMessage();
$message->setTo([$email => $user->getDisplayName()]);
$message->setFrom([Util::getDefaultEmailAddress('noreply') => $this->l10n->t('Deck Workflow Automation')]);
$message->useTemplate($template);
$failedRecipients = $this->mailer->send($message);
if (!empty($failedRecipients)) {
$this->logger->error('Deactivation mail for workflow ' . $workflow->getId() . ' failed for: ' . implode(', ', $failedRecipients), [
'app' => 'workflow_deck_automation',
]);
}
} catch (Throwable $e) {
$this->logger->error('Could not send deactivation mail for workflow ' . $workflow->getId() . ': ' . $e->getMessage(), [
'app' => 'workflow_deck_automation',
'exception' => $e,
]);
}
}
/**
* Appends the card's description, if it has one.
*