feat(app-dev): surface metadata diff in dev sync and name failing migration actions (#21249)

Split out of #21240.

- Render the applied metadata changes (created/updated/deleted +
identifiers) in the dev sync output instead of a bare `✓ Synced`.
- Include the failing entity's `universalIdentifier` in
`WorkspaceMigrationRunnerException` messages so conflicts are
diagnosable.

<img width="637" height="114" alt="image"
src="https://github.com/user-attachments/assets/61422a16-370c-4e9b-a2f6-c29ce17f3b1b"
/>

<img width="497" height="104" alt="image"
src="https://github.com/user-attachments/assets/d493c398-da29-49c9-ac5e-aa0f26cd7389"
/>

<img width="593" height="127" alt="image"
src="https://github.com/user-attachments/assets/15e26edc-c0e4-4427-bd34-909040e970c9"
/>

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
martmull
2026-06-05 16:35:50 +02:00
committed by GitHub
parent f72898063a
commit f20d04eb6e
13 changed files with 364 additions and 5 deletions
@@ -0,0 +1,45 @@
import { type AllUniversalWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration-action-common';
import {
WorkspaceMigrationRunnerException,
WorkspaceMigrationRunnerExceptionCode,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/exceptions/workspace-migration-runner.exception';
describe('WorkspaceMigrationRunnerException', () => {
it('includes the universal identifier of a failed create action in the message', () => {
const action = {
type: 'create',
metadataName: 'fieldMetadata',
flatEntity: {
universalIdentifier: '20202020-6736-4337-b5c4-8b39fae325a5',
},
} as unknown as AllUniversalWorkspaceMigrationAction;
const exception = new WorkspaceMigrationRunnerException({
code: WorkspaceMigrationRunnerExceptionCode.EXECUTION_FAILED,
action,
errors: {},
});
expect(exception.message).toBe(
"Migration action 'create' for 'fieldMetadata' (universalIdentifier: 20202020-6736-4337-b5c4-8b39fae325a5) failed",
);
});
it('includes the universal identifier of a failed delete action in the message', () => {
const action = {
type: 'delete',
metadataName: 'pageLayout',
universalIdentifier: 'uid-page-layout',
} as unknown as AllUniversalWorkspaceMigrationAction;
const exception = new WorkspaceMigrationRunnerException({
code: WorkspaceMigrationRunnerExceptionCode.EXECUTION_FAILED,
action,
errors: {},
});
expect(exception.message).toBe(
"Migration action 'delete' for 'pageLayout' (universalIdentifier: uid-page-layout) failed",
);
});
});
@@ -34,6 +34,25 @@ export type WorkspaceMigrationRunnerExecutionErrors = {
actionTranspilation?: Error;
};
const getActionUniversalIdentifierOrThrow = (
action: AllUniversalWorkspaceMigrationAction,
): string => {
if (action.type === 'create') {
const universalIdentifier = action.flatEntity?.universalIdentifier;
if (!universalIdentifier) {
throw new WorkspaceMigrationRunnerException({
message: `Missing universalIdentifier on create action for '${action.metadataName}'`,
code: WorkspaceMigrationRunnerExceptionCode.INTERNAL_SERVER_ERROR,
});
}
return universalIdentifier;
}
return action.universalIdentifier;
};
const {
// oxlint-disable-next-line unused-imports/no-unused-vars
EXECUTION_FAILED: WorkspaceMigrationRunnerExceptionExecutionFailedCode,
@@ -62,8 +81,13 @@ export class WorkspaceMigrationRunnerException extends CustomError {
constructor(args: WorkspaceMigrationRunnerExceptionConstructorArgs) {
if (args.code === WorkspaceMigrationRunnerExceptionCode.EXECUTION_FAILED) {
const universalIdentifier = getActionUniversalIdentifierOrThrow(
args.action,
);
const identifierClause = ` (universalIdentifier: ${universalIdentifier})`;
super(
`Migration action '${args.action.type}' for '${args.action.metadataName}' failed`,
`Migration action '${args.action.type}' for '${args.action.metadataName}'${identifierClause} failed`,
);
this.code = args.code;