Three admin surfaces of the same family: each one either acts on a run without asking whether it may, or reports a change without reading whether it happened.
The bulk action deletes running process instances. orchestra_vbo's Delete process calls delete() on every selected instance with no state check, and its access() looks only at the administer orchestra permission. EntityInterface::delete() runs no access check, so the action deletes running instances and cascades through their tokens, variables, incidents, work items and subprocess children. Every other doorway refuses exactly that: the instance access hook, the instance page, the instance list form and the delete confirm form all say a running instance must be canceled first.
Start on the model list answers a 500 instead of a refusal. The engine refuses three starts: a disabled workflow, one whose start node is unset or names a node that no longer exists, and one scoped to another tenant than the administrator is acting in. The list withholds the action for the first only, so the other two are a click away, and the controller calls the engine with no check, so each of them escapes as an uncaught LogicException. A white screen where a sentence would do.
Three completion surfaces announce a completion that did not happen. WorkItemManager::complete() answers FALSE for two real no-ops, a task that is already terminal and a task whose lock could not be taken, and the inbox controller, the inbox task form and the attached-content task screen all discard that answer and announce success. The holder is told the step is done while the run is still parked on it. The pull-side surfaces were fixed in #3621881: Report what an action actually did, and stop Hold erasing the retention ages; these are the sites that fix missed.
What changes:
- Delete process refuses a running instance from access(), so Views Bulk Operations lists the row with the reason and still processes the rest of the selection; execute() re-checks the state before deleting.
- The Start controller asks DefinitionResolver::getStartRefusal() before it acts, catches the race behind that answer, and warns with the reason instead of throwing. It logs the reason as well, the way the webform handler already logs a start it could not make. Withholding the action is deliberately left to the disabled case alone: this list carries a Status column, so a missing Start reads as the reason for a disabled workflow and as nothing at all for one whose start node was deleted, and that message is then the only place its author finds out.
- The three completion surfaces read the boolean and warn that nothing was recorded, the way the pull-side surfaces now do.
Kernel coverage for each: a running instance survives the bulk delete and the refusal names why; a disabled workflow is offered no Start while one with no valid start node keeps it, and the controller answers that one with the reason rather than an exception; an already-terminal task and a task whose lock is refused both produce the warning rather than the success message, on all three surfaces.
AI-Generated: Yes (Claude Code was used to help draft this issue summary and to write the code and tests on the merge request. I reviewed and ran the work myself before posting it.)
Issue fork orchestra-3622165
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #3
mably commentedComment #4
mably commentedComment #5
mably commented