feat(core): Add workflow.deactivate external hook (#34746)

This commit is contained in:
Lorent Lempereur
2026-07-24 09:15:28 +00:00
committed by GitHub
parent b7dcfc84cb
commit 697174f3f8
4 changed files with 69 additions and 0 deletions
@@ -0,0 +1,15 @@
import { BadRequestError } from './bad-request.error';
/**
* Error thrown when a workflow fails to deactivate, e.g. because a
* `workflow.deactivate` external hook rejected the operation.
*/
export class WorkflowDeactivationBadRequestError extends BadRequestError {
constructor(
message: string,
readonly meta: { description?: string } = {},
) {
super(message);
this.name = 'WorkflowDeactivationBadRequestError';
}
}
+1
View File
@@ -71,6 +71,7 @@ type ExternalHooksMap = {
'workflow.create': [createdWorkflow: IWorkflowBase];
'workflow.afterCreate': [createdWorkflow: IWorkflowBase];
'workflow.activate': [updatedWorkflow: IWorkflowBase];
'workflow.deactivate': [deactivatedWorkflow: IWorkflowBase];
'workflow.update': [updatedWorkflow: IWorkflowBase];
'workflow.afterUpdate': [updatedWorkflow: IWorkflowBase];
'workflow.delete': [workflowId: string];
@@ -22,6 +22,7 @@ import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { ConflictError } from '@/errors/response-errors/conflict.error';
import { UnprocessableRequestError } from '@/errors/response-errors/unprocessable.error';
import { WorkflowActivationBadRequestError } from '@/errors/response-errors/workflow-activation-bad-request.error';
import { WorkflowDeactivationBadRequestError } from '@/errors/response-errors/workflow-deactivation-bad-request.error';
import type { EventService } from '@/events/event.service';
import type { ExternalHooks } from '@/external-hooks';
import type { RedactionEnforcementService } from '@/modules/redaction/redaction-enforcement.service';
@@ -1258,6 +1259,49 @@ describe('WorkflowService', () => {
// in-memory teardown is left to the leader, not run here
expect(activeWorkflowManagerMock.remove).not.toHaveBeenCalled();
});
test('deactivation blocked by hook leaves the workflow published', async () => {
const workflow = makeWorkflowEntity({ activeVersionId: PREVIOUS_VERSION_ID });
workflowFinderServiceMock.findWorkflowForUser.mockResolvedValue(workflow);
externalHooksMock.run.mockRejectedValue(new Error('Code freeze in effect'));
const user = mock<User>();
await expect(workflowService.deactivateWorkflow(user, WORKFLOW_ID)).rejects.toBeInstanceOf(
WorkflowDeactivationBadRequestError,
);
expect(workflow.active).toBe(true);
expect(workflow.activeVersionId).toBe(PREVIOUS_VERSION_ID);
expect(activeWorkflowManagerMock.remove).not.toHaveBeenCalled();
expect(workflowRepositoryMock.update).not.toHaveBeenCalled();
expect(workflowPublishHistoryRepositoryMock.addRecord).not.toHaveBeenCalled();
});
test('hook receives the workflow being deactivated', async () => {
const workflow = makeWorkflowEntity({ activeVersionId: PREVIOUS_VERSION_ID });
workflowFinderServiceMock.findWorkflowForUser.mockResolvedValue(workflow);
externalHooksMock.run.mockResolvedValue(undefined);
const user = mock<User>();
await workflowService.deactivateWorkflow(user, WORKFLOW_ID);
expect(externalHooksMock.run).toHaveBeenCalledWith('workflow.deactivate', [workflow]);
});
test('does not run the hook when the workflow is already inactive', async () => {
const workflow = makeWorkflowEntity({ active: false, activeVersionId: null });
workflowFinderServiceMock.findWorkflowForUser.mockResolvedValue(workflow);
const user = mock<User>();
await workflowService.deactivateWorkflow(user, WORKFLOW_ID);
expect(externalHooksMock.run).not.toHaveBeenCalled();
});
});
describe('delete()', () => {
@@ -32,6 +32,7 @@ import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { ConflictError } from '@/errors/response-errors/conflict.error';
import { NotFoundError } from '@/errors/response-errors/not-found.error';
import { WorkflowActivationBadRequestError } from '@/errors/response-errors/workflow-activation-bad-request.error';
import { WorkflowDeactivationBadRequestError } from '@/errors/response-errors/workflow-deactivation-bad-request.error';
import { WorkflowValidationError } from '@/errors/response-errors/workflow-validation.error';
import { WorkflowHistoryVersionNotFoundError } from '@/errors/workflow-history-version-not-found.error';
import { EventService } from '@/events/event.service';
@@ -983,6 +984,14 @@ export class WorkflowService {
await this._detectConflicts(workflow, options.expectedChecksum);
}
try {
await this.externalHooks.run('workflow.deactivate', [workflow]);
} catch (error) {
throw new WorkflowDeactivationBadRequestError(ensureError(error).message, {
description: getErrorDescription(error),
});
}
if (this.globalConfig.workflows.useWorkflowPublicationService) {
await this._unpublishViaOutbox(user, workflowId, deactivatedVersionId, workflow.updatedAt);
} else {