diff --git a/lib/workers/repository/model/commit-message.ts b/lib/workers/repository/model/commit-message.ts
index 0344e5b6471fc0ee8eb2f321ec58d84ae11edfb6..b909f5bc1d837da52d340f71af9147d1b35549ca 100644
--- a/lib/workers/repository/model/commit-message.ts
+++ b/lib/workers/repository/model/commit-message.ts
@@ -60,6 +60,10 @@ export abstract class CommitMessage {
     this._footer = this.normalizeInput(value);
   }
 
+  get subject(): string {
+    return this._subject;
+  }
+
   set subject(value: string) {
     this._subject = this.normalizeInput(value);
     this._subject = this._subject?.replace(
diff --git a/lib/workers/repository/model/semantic-commit-message.spec.ts b/lib/workers/repository/model/semantic-commit-message.spec.ts
index ffff30f3258f54c839708d3e335c3e3e6dbbaf59..e2629fcc02edd3575ccca58212b165ba07bd5c46 100644
--- a/lib/workers/repository/model/semantic-commit-message.spec.ts
+++ b/lib/workers/repository/model/semantic-commit-message.spec.ts
@@ -25,6 +25,15 @@ describe('workers/repository/model/semantic-commit-message', () => {
     expect(message.toString()).toBe('fix(scope): test');
   });
 
+  it('should transform to lowercase only first letter', () => {
+    const message = new SemanticCommitMessage();
+    message.subject = 'Update My Org dependencies';
+    message.type = 'fix';
+    message.scope = 'deps ';
+
+    expect(message.toString()).toBe('fix(deps): update My Org dependencies');
+  });
+
   it('should create instance from string without scope', () => {
     const instance = SemanticCommitMessage.fromString('feat: ticket 123');
 
diff --git a/lib/workers/repository/updates/generate.spec.ts b/lib/workers/repository/updates/generate.spec.ts
index a6fa8195bf381bfd1754a62ddc07778764a84b14..87d7c55786962a9d652b5694db1e430c8b07dd76 100644
--- a/lib/workers/repository/updates/generate.spec.ts
+++ b/lib/workers/repository/updates/generate.spec.ts
@@ -506,7 +506,7 @@ describe('workers/repository/updates/generate', () => {
         } as BranchUpgradeConfig,
       ];
       const res = generateBranchConfig(branch);
-      expect(res.prTitle).toBe('chore(): update dependency some-dep to v1.2.0');
+      expect(res.prTitle).toBe('chore: update dependency some-dep to v1.2.0');
     });
 
     it('scopes monorepo commits with nested package files using parent directory', () => {
@@ -915,5 +915,37 @@ describe('workers/repository/updates/generate', () => {
         '`1.1.1` (+1)',
       ]);
     });
+
+    it('fixes commit message with body', () => {
+      const branch: BranchUpgradeConfig[] = [
+        {
+          manager: 'some-manager',
+          branchName: 'some-branch',
+          commitMessage: 'update to vv1.2.0',
+          commitBody: 'some body',
+        },
+      ];
+      const res = generateBranchConfig(branch);
+      expect(res.commitMessage).toBe('Update to v1.2.0\n\nsome body');
+    });
+
+    it('generates semantic commit message properly', () => {
+      const branch: BranchUpgradeConfig[] = [
+        {
+          ...defaultConfig,
+          manager: 'some-manager',
+          branchName: 'some-branch',
+          semanticCommits: 'enabled',
+          semanticCommitType: 'chore',
+          semanticCommitScope: 'deps',
+          depName: 'some-dep',
+          newValue: '1.2.0',
+        } as BranchUpgradeConfig,
+      ];
+      const res = generateBranchConfig(branch);
+      expect(res.commitMessage).toBe(
+        'chore(deps): update dependency some-dep to 1.2.0'
+      );
+    });
   });
 });
diff --git a/lib/workers/repository/updates/generate.ts b/lib/workers/repository/updates/generate.ts
index 04af7f203a1bee9f7264b39ba6ead3da4130fd24..2f6c7c2826e60906c386cbf2cbd0f78cd728cdbf 100644
--- a/lib/workers/repository/updates/generate.ts
+++ b/lib/workers/repository/updates/generate.ts
@@ -6,11 +6,12 @@ import semver from 'semver';
 import { mergeChildConfig } from '../../../config';
 import { CONFIG_SECRETS_EXPOSED } from '../../../constants/error-messages';
 import { logger } from '../../../logger';
-import { newlineRegex, regEx } from '../../../util/regex';
+import { regEx } from '../../../util/regex';
 import { sanitize } from '../../../util/sanitize';
 import * as template from '../../../util/template';
 import type { BranchConfig, BranchUpgradeConfig } from '../../types';
-import { CommitMessage } from '../model/commit-message';
+import { CommitMessageFactory } from '../model/commit-message-factory';
+import { SemanticCommitMessage } from '../model/semantic-commit-message';
 
 function isTypesGroup(branchUpgrades: BranchUpgradeConfig[]): boolean {
   return (
@@ -167,28 +168,27 @@ export function generateBranchConfig(
     } else if (semver.valid(toVersions[0])) {
       upgrade.isRange = false;
     }
+    const commitMessageFactory = new CommitMessageFactory(upgrade);
+    const commitMessage = commitMessageFactory.create();
     // Use templates to generate strings
-    if (upgrade.semanticCommits === 'enabled' && !upgrade.commitMessagePrefix) {
+    if (
+      SemanticCommitMessage.is(commitMessage) &&
+      upgrade.semanticCommitScope
+    ) {
       logger.trace('Upgrade has semantic commits enabled');
-      let semanticPrefix = upgrade.semanticCommitType;
-      if (upgrade.semanticCommitScope) {
-        semanticPrefix += `(${template.compile(
-          upgrade.semanticCommitScope,
-          upgrade
-        )})`;
-      }
-      upgrade.commitMessagePrefix = CommitMessage.formatPrefix(semanticPrefix!);
-      upgrade.toLowerCase =
-        regEx(/[A-Z]/).exec(upgrade.semanticCommitType!) === null &&
-        !upgrade.semanticCommitType!.startsWith(':');
+      commitMessage.scope = template.compile(
+        upgrade.semanticCommitScope,
+        upgrade
+      );
     }
     // Compile a few times in case there are nested templates
-    upgrade.commitMessage = template.compile(
+    commitMessage.subject = template.compile(
       upgrade.commitMessage ?? '',
       upgrade
     );
-    upgrade.commitMessage = template.compile(upgrade.commitMessage, upgrade);
-    upgrade.commitMessage = template.compile(upgrade.commitMessage, upgrade);
+    commitMessage.subject = template.compile(commitMessage.subject, upgrade);
+    commitMessage.subject = template.compile(commitMessage.subject, upgrade);
+    upgrade.commitMessage = commitMessage.toString();
     // istanbul ignore if
     if (upgrade.commitMessage !== sanitize(upgrade.commitMessage)) {
       logger.debug(
@@ -197,23 +197,14 @@ export function generateBranchConfig(
       );
       throw new Error(CONFIG_SECRETS_EXPOSED);
     }
-    upgrade.commitMessage = upgrade.commitMessage.trim(); // Trim exterior whitespace
-    upgrade.commitMessage = upgrade.commitMessage.replace(regEx(/\s+/g), ' '); // Trim extra whitespace inside string
-    upgrade.commitMessage = upgrade.commitMessage.replace(
+    commitMessage.subject = commitMessage.subject.replace(
       regEx(/to vv(\d)/),
       'to v$1'
     );
-    if (upgrade.toLowerCase) {
-      // We only need to lowercase the first line
-      const splitMessage = upgrade.commitMessage.split(newlineRegex);
-      splitMessage[0] = splitMessage[0].toLowerCase();
-      upgrade.commitMessage = splitMessage.join('\n');
-    }
+    upgrade.commitMessage = commitMessage.toString();
     if (upgrade.commitBody) {
-      upgrade.commitMessage = `${upgrade.commitMessage}\n\n${template.compile(
-        upgrade.commitBody,
-        upgrade
-      )}`;
+      commitMessage.body = template.compile(upgrade.commitBody, upgrade);
+      upgrade.commitMessage = commitMessage.toString();
     }
     logger.trace(`commitMessage: ` + JSON.stringify(upgrade.commitMessage));
     if (upgrade.prTitle) {
@@ -235,7 +226,7 @@ export function generateBranchConfig(
         upgrade.prTitle = upgrade.prTitle.toLowerCase();
       }
     } else {
-      [upgrade.prTitle] = upgrade.commitMessage.split(newlineRegex);
+      upgrade.prTitle = commitMessage.title;
     }
     upgrade.prTitle += upgrade.hasBaseBranches ? ' ({{baseBranch}})' : '';
     if (upgrade.isGroup) {