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
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:
@@ -26,6 +26,10 @@ use Throwable;
|
||||
* the standard Nextcloud pattern for multi-user cron jobs.
|
||||
*/
|
||||
class WorkflowRunner {
|
||||
public const TARGET_BOARD = 'board';
|
||||
public const TARGET_SOURCE_STACK = 'sourceStack';
|
||||
public const TARGET_TARGET_STACK = 'targetStack';
|
||||
|
||||
private ?IUser $impersonationRestore = null;
|
||||
|
||||
public function __construct(
|
||||
@@ -110,6 +114,23 @@ class WorkflowRunner {
|
||||
}
|
||||
|
||||
private function runWorkflow(IUser $user, Workflow $workflow): void {
|
||||
try {
|
||||
$brokenTarget = $this->findBrokenTarget($workflow);
|
||||
} catch (DeckUnavailableException $e) {
|
||||
// Could not determine it either way — leave the workflow alone
|
||||
// rather than disabling it over a temporary Deck problem.
|
||||
$this->logger->warning('Could not verify targets of workflow {id}: ' . $e->getMessage(), [
|
||||
'app' => 'workflow_deck_automation',
|
||||
'id' => $workflow->getId(),
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($brokenTarget !== null) {
|
||||
$this->disableBrokenWorkflow($user, $workflow, $brokenTarget);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$cards = $this->deckService->getActiveCardsInStack($workflow->getSourceStackId());
|
||||
} catch (DeckUnavailableException $e) {
|
||||
@@ -141,6 +162,54 @@ class WorkflowRunner {
|
||||
$this->workflowMapper->update($workflow);
|
||||
}
|
||||
|
||||
/**
|
||||
* Which of the workflow's Deck references no longer exists, if any.
|
||||
*
|
||||
* Deleting a board or stack leaves the workflow row untouched — there is
|
||||
* no foreign key — and Deck keeps the orphaned cards readable until its
|
||||
* own DeleteCron purges them, so the workflow would otherwise keep
|
||||
* failing on every single run.
|
||||
*
|
||||
* @return self::TARGET_*|null
|
||||
* @throws DeckUnavailableException if Deck could not be asked
|
||||
*/
|
||||
private function findBrokenTarget(Workflow $workflow): ?string {
|
||||
$stackIds = $this->deckService->findStackIds($workflow->getBoardId());
|
||||
if ($stackIds === null) {
|
||||
return self::TARGET_BOARD;
|
||||
}
|
||||
|
||||
if (!in_array($workflow->getSourceStackId(), $stackIds, true)) {
|
||||
return self::TARGET_SOURCE_STACK;
|
||||
}
|
||||
if (!in_array($workflow->getTargetStackId(), $stackIds, true)) {
|
||||
return self::TARGET_TARGET_STACK;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param self::TARGET_* $brokenTarget
|
||||
*/
|
||||
private function disableBrokenWorkflow(IUser $user, Workflow $workflow, string $brokenTarget): void {
|
||||
$workflow->setEnabled(false);
|
||||
$workflow->setLastRun($this->timeFactory->getDateTime());
|
||||
$this->workflowMapper->update($workflow);
|
||||
|
||||
$this->logger->warning('Disabled workflow {id}: its {target} no longer exists', [
|
||||
'app' => 'workflow_deck_automation',
|
||||
'id' => $workflow->getId(),
|
||||
'target' => $brokenTarget,
|
||||
]);
|
||||
|
||||
// Sent regardless of the workflow's notifyEmail setting: that flag is
|
||||
// about moved cards, this is a one-off notice that the automation the
|
||||
// user configured has been switched off. It stays one-off because a
|
||||
// disabled workflow is not picked up again.
|
||||
$this->mailer->sendWorkflowDisabledNotification($user, $workflow, $brokenTarget);
|
||||
}
|
||||
|
||||
private function moveAndNotify(IUser $user, Workflow $workflow, Card $card): void {
|
||||
try {
|
||||
$this->deckService->moveCard($card->getId(), $workflow->getTargetStackId());
|
||||
|
||||
Reference in New Issue
Block a user