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:
@@ -10,6 +10,7 @@ use OCA\Deck\Service\BoardService;
|
||||
use OCA\Deck\Service\CardService;
|
||||
use OCA\Deck\Service\StackService;
|
||||
use OCP\App\IAppManager;
|
||||
use OCP\AppFramework\Db\DoesNotExistException;
|
||||
use OCP\IGroupManager;
|
||||
use OCP\IUser;
|
||||
use OCP\IUserManager;
|
||||
@@ -57,6 +58,13 @@ class DeckIntegrationService {
|
||||
|
||||
/**
|
||||
* @return array<int, array{id: int, title: string}>
|
||||
*
|
||||
* Only ever call this from a *request*: Deck injects the current user id
|
||||
* into BoardService as a plain string frozen at construction time, so in
|
||||
* the background job (one process, many users, a container-cached
|
||||
* BoardService) this would answer for the wrong user — or for none at
|
||||
* all. The job uses findStackIds() instead, which goes through Deck's
|
||||
* PermissionService and therefore reads the live session.
|
||||
*/
|
||||
public function listBoardsForCurrentUser(): array {
|
||||
$boards = $this->call(function () {
|
||||
@@ -110,11 +118,61 @@ class DeckIntegrationService {
|
||||
}, []);
|
||||
|
||||
return array_map(
|
||||
static fn ($stack) => ['id' => $stack->getId(), 'title' => $stack->getTitle()],
|
||||
static fn ($stack) => ['id' => (int)$stack->getId(), 'title' => $stack->getTitle()],
|
||||
$stacks,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ids of the board's stacks, for deciding whether a stored workflow still
|
||||
* points at anything real.
|
||||
*
|
||||
* Returns `null` when the board itself is gone or not readable for the
|
||||
* current user — `StackService::findAll()` runs a Deck permission check
|
||||
* first, and that one reads the *live* session, which is what makes this
|
||||
* usable from the impersonating background job. Any other failure throws,
|
||||
* because "Deck is broken right now" must never be mistaken for "the user
|
||||
* deleted this board".
|
||||
*
|
||||
* @return int[]|null
|
||||
* @throws DeckUnavailableException
|
||||
*/
|
||||
public function findStackIds(int $boardId): ?array {
|
||||
try {
|
||||
$stacks = $this->resolve(StackService::class)->findAll($boardId);
|
||||
} catch (DeckUnavailableException $e) {
|
||||
throw $e;
|
||||
} catch (Throwable $e) {
|
||||
if ($this->isMissingOrForbidden($e)) {
|
||||
return null;
|
||||
}
|
||||
$this->logger->error('Could not list stacks of board ' . $boardId . ': ' . $e->getMessage(), [
|
||||
'app' => 'workflow_deck_automation',
|
||||
'exception' => $e,
|
||||
]);
|
||||
throw new DeckUnavailableException('Deck call failed: ' . $e->getMessage(), 0, $e);
|
||||
}
|
||||
|
||||
return array_map(static fn ($stack) => (int)$stack->getId(), $stacks);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deck's own exception classes are referenced by name: `is_a()` with a
|
||||
* string simply returns false when the class does not exist, so a missing
|
||||
* or renamed Deck degrades to "unknown error" instead of fataling.
|
||||
*/
|
||||
private function isMissingOrForbidden(Throwable $e): bool {
|
||||
if ($e instanceof DoesNotExistException) {
|
||||
return true;
|
||||
}
|
||||
foreach (['OCA\Deck\NoPermissionException', 'OCA\Deck\NotFoundException'] as $class) {
|
||||
if (is_a($e, $class)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array{id: int, title: string, color: string|null}>
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user