diff --git a/lib/workers/repository/model/commit-message.spec.ts b/lib/workers/repository/model/commit-message.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..9290af0bbabf45a6f949770e235e4b9b44aedddd
--- /dev/null
+++ b/lib/workers/repository/model/commit-message.spec.ts
@@ -0,0 +1,58 @@
+import { getName } from '../../../../test/util';
+import { CommitMessage } from './commit-message';
+
+describe(getName(), () => {
+  describe('CommitMessage', () => {
+    const TEST_CASES: ReadonlyArray<
+      [message: string, prefix: string | undefined, result: string]
+    > = [
+      ['test', undefined, 'Test'],
+      ['test', '', 'Test'],
+      ['  test  ', '  ', 'Test'],
+      ['test', 'fix', 'fix: test'],
+      ['test', 'fix:', 'fix: test'],
+    ];
+
+    it('has colon character separator', () => {
+      expect(CommitMessage.SEPARATOR).toBe(':');
+    });
+
+    it.each(TEST_CASES)(
+      'given %p and %p as arguments, returns %p',
+      (message, prefix, result) => {
+        const commitMessage = new CommitMessage(message);
+        commitMessage.setCustomPrefix(prefix);
+
+        expect(commitMessage.toString()).toEqual(result);
+      }
+    );
+
+    it('should handle not defined semantic prefix', () => {
+      const message = new CommitMessage('test');
+      message.setSemanticPrefix();
+
+      expect(message.toString()).toBe('Test');
+    });
+
+    it('should handle empty semantic prefix', () => {
+      const message = new CommitMessage('test');
+      message.setSemanticPrefix('  ', '  ');
+
+      expect(message.toString()).toBe('Test');
+    });
+
+    it('should format sematic prefix', () => {
+      const message = new CommitMessage('test');
+      message.setSemanticPrefix(' fix ');
+
+      expect(message.toString()).toBe('fix: test');
+    });
+
+    it('should format sematic prefix with scope', () => {
+      const message = new CommitMessage('test');
+      message.setSemanticPrefix(' fix ', ' scope ');
+
+      expect(message.toString()).toBe('fix(scope): test');
+    });
+  });
+});
diff --git a/lib/workers/repository/model/commit-message.ts b/lib/workers/repository/model/commit-message.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f567b5615e94a7d9164a1200c3debb6acb03aa39
--- /dev/null
+++ b/lib/workers/repository/model/commit-message.ts
@@ -0,0 +1,54 @@
+export class CommitMessage {
+  public static readonly SEPARATOR: string = ':';
+
+  private message: string;
+
+  private prefix: string;
+
+  constructor(message = '') {
+    this.setMessage(message);
+  }
+
+  public static formatPrefix(prefix: string): string {
+    if (!prefix) {
+      return '';
+    }
+
+    if (prefix.endsWith(CommitMessage.SEPARATOR)) {
+      return prefix;
+    }
+
+    return `${prefix}${CommitMessage.SEPARATOR}`;
+  }
+
+  public setMessage(message: string): void {
+    this.message = message.trim();
+  }
+
+  public setCustomPrefix(prefix = ''): void {
+    this.prefix = prefix.trim();
+  }
+
+  public setSemanticPrefix(type = '', scope = ''): void {
+    this.prefix = type.trim();
+
+    if (scope.trim()) {
+      this.prefix += `(${scope.trim()})`;
+    }
+  }
+
+  public toString(): string {
+    const prefix = CommitMessage.formatPrefix(this.prefix);
+    const message = this.formatMessage();
+
+    return [prefix, message].join(' ').trim();
+  }
+
+  private formatMessage(): string {
+    if (this.prefix) {
+      return this.message;
+    }
+
+    return this.message.charAt(0).toUpperCase() + this.message.slice(1);
+  }
+}
diff --git a/lib/workers/repository/onboarding/branch/commit-message.ts b/lib/workers/repository/onboarding/branch/commit-message.ts
new file mode 100644
index 0000000000000000000000000000000000000000..88c36c3b556058b73eb701b0e4259db729b4fee0
--- /dev/null
+++ b/lib/workers/repository/onboarding/branch/commit-message.ts
@@ -0,0 +1,41 @@
+import { RenovateConfig } from '../../../../config/types';
+import { CommitMessage } from '../../model/commit-message';
+
+export class OnboardingCommitMessageFactory {
+  private readonly config: RenovateConfig;
+
+  private readonly configFile: string;
+
+  constructor(config: RenovateConfig, configFile: string) {
+    this.config = config;
+    this.configFile = configFile;
+  }
+
+  create(): CommitMessage {
+    const {
+      commitMessagePrefix,
+      onboardingCommitMessage,
+      semanticCommitScope,
+      semanticCommitType,
+    } = this.config;
+    const commitMessage = new CommitMessage();
+
+    if (commitMessagePrefix) {
+      commitMessage.setCustomPrefix(commitMessagePrefix);
+    } else if (this.areSemanticCommitsEnabled()) {
+      commitMessage.setSemanticPrefix(semanticCommitType, semanticCommitScope);
+    }
+
+    if (onboardingCommitMessage) {
+      commitMessage.setMessage(onboardingCommitMessage);
+    } else {
+      commitMessage.setMessage(`add ${this.configFile}`);
+    }
+
+    return commitMessage;
+  }
+
+  private areSemanticCommitsEnabled(): boolean {
+    return this.config.semanticCommits === 'enabled';
+  }
+}
diff --git a/lib/workers/repository/onboarding/branch/create.spec.ts b/lib/workers/repository/onboarding/branch/create.spec.ts
index decbee29aec66891cfd9cd872fdf5a1d10c8781a..3ea76b528a2f658e740a29a372c35e74c47fa0ad 100644
--- a/lib/workers/repository/onboarding/branch/create.spec.ts
+++ b/lib/workers/repository/onboarding/branch/create.spec.ts
@@ -1,6 +1,6 @@
 import { RenovateConfig, getConfig, getName } from '../../../../../test/util';
 import { commitFiles } from '../../../../util/git';
-import { COMMIT_MESSAGE_PREFIX_SEPARATOR } from '../../util/commit-message';
+import { CommitMessage } from '../../model/commit-message';
 import { createOnboardingBranch } from './create';
 
 jest.mock('../../../../util/git');
@@ -54,7 +54,7 @@ describe(getName(), () => {
         await createOnboardingBranch(config);
         expect(commitFiles).toHaveBeenCalledWith(
           buildExpectedCommitFilesArgument(
-            `${prefix}${COMMIT_MESSAGE_PREFIX_SEPARATOR} add renovate.json`
+            `${prefix}${CommitMessage.SEPARATOR} add renovate.json`
           )
         );
       });
@@ -67,7 +67,7 @@ describe(getName(), () => {
         await createOnboardingBranch(config);
         expect(commitFiles).toHaveBeenCalledWith(
           buildExpectedCommitFilesArgument(
-            `${prefix}${COMMIT_MESSAGE_PREFIX_SEPARATOR} ${message}`
+            `${prefix}${CommitMessage.SEPARATOR} ${message}`
           )
         );
       });
@@ -79,7 +79,7 @@ describe(getName(), () => {
         await createOnboardingBranch(config);
         expect(commitFiles).toHaveBeenCalledWith(
           buildExpectedCommitFilesArgument(
-            `${prefix}${COMMIT_MESSAGE_PREFIX_SEPARATOR} add renovate.json`
+            `${prefix}${CommitMessage.SEPARATOR} add renovate.json`
           )
         );
       });
@@ -92,7 +92,7 @@ describe(getName(), () => {
         await createOnboardingBranch(config);
         expect(commitFiles).toHaveBeenCalledWith(
           buildExpectedCommitFilesArgument(
-            `${prefix}${COMMIT_MESSAGE_PREFIX_SEPARATOR} ${message}`
+            `${prefix}${CommitMessage.SEPARATOR} ${message}`
           )
         );
       });
@@ -105,7 +105,7 @@ describe(getName(), () => {
         await createOnboardingBranch(config);
         expect(commitFiles).toHaveBeenCalledWith(
           buildExpectedCommitFilesArgument(
-            `${prefix}${COMMIT_MESSAGE_PREFIX_SEPARATOR} add renovate.json`
+            `${prefix}${CommitMessage.SEPARATOR} add renovate.json`
           )
         );
       });
@@ -116,7 +116,7 @@ describe(getName(), () => {
         await createOnboardingBranch(config);
         expect(commitFiles).toHaveBeenCalledWith(
           buildExpectedCommitFilesArgument(
-            `${prefix}${COMMIT_MESSAGE_PREFIX_SEPARATOR} add renovate.json`
+            `${prefix}${CommitMessage.SEPARATOR} add renovate.json`
           )
         );
       });
@@ -127,7 +127,7 @@ describe(getName(), () => {
         await createOnboardingBranch(config);
         expect(commitFiles).toHaveBeenCalledWith(
           buildExpectedCommitFilesArgument(
-            `${prefix}${COMMIT_MESSAGE_PREFIX_SEPARATOR} add ${config.onboardingConfigFileName}`,
+            `${prefix}${CommitMessage.SEPARATOR} add ${config.onboardingConfigFileName}`,
             config.onboardingConfigFileName
           )
         );
@@ -139,7 +139,7 @@ describe(getName(), () => {
         await createOnboardingBranch(config);
         expect(commitFiles).toHaveBeenCalledWith(
           buildExpectedCommitFilesArgument(
-            `${prefix}${COMMIT_MESSAGE_PREFIX_SEPARATOR} add ${config.onboardingConfigFileName}`,
+            `${prefix}${CommitMessage.SEPARATOR} add ${config.onboardingConfigFileName}`,
             config.onboardingConfigFileName
           )
         );
diff --git a/lib/workers/repository/onboarding/branch/create.ts b/lib/workers/repository/onboarding/branch/create.ts
index e670134c6871484ae0a831cee38174c06bb9fa1a..c819f09bec2a847f798b00846f26d3897064f9b7 100644
--- a/lib/workers/repository/onboarding/branch/create.ts
+++ b/lib/workers/repository/onboarding/branch/create.ts
@@ -3,7 +3,7 @@ import { configFileNames } from '../../../../config/app-strings';
 import type { RenovateConfig } from '../../../../config/types';
 import { logger } from '../../../../logger';
 import { commitFiles } from '../../../../util/git';
-import { formatCommitMessagePrefix } from '../../util/commit-message';
+import { OnboardingCommitMessageFactory } from './commit-message';
 import { getOnboardingConfigContents } from './config';
 
 const defaultConfigFile = configFileNames[0];
@@ -19,30 +19,11 @@ export async function createOnboardingBranch(
     ? config.onboardingConfigFileName
     : defaultConfigFile;
 
-  let commitMessagePrefix = '';
-  if (config.commitMessagePrefix) {
-    commitMessagePrefix = config.commitMessagePrefix;
-  } else if (config.semanticCommits === 'enabled') {
-    commitMessagePrefix = config.semanticCommitType;
-    if (config.semanticCommitScope) {
-      commitMessagePrefix += `(${config.semanticCommitScope})`;
-    }
-  }
-  if (commitMessagePrefix) {
-    commitMessagePrefix = formatCommitMessagePrefix(commitMessagePrefix);
-  }
-
-  let onboardingCommitMessage: string;
-  if (config.onboardingCommitMessage) {
-    onboardingCommitMessage = config.onboardingCommitMessage;
-  } else {
-    onboardingCommitMessage = `${
-      commitMessagePrefix ? 'add' : 'Add'
-    } ${configFile}`;
-  }
-
-  const commitMessage =
-    `${commitMessagePrefix} ${onboardingCommitMessage}`.trim();
+  const commitMessageFactory = new OnboardingCommitMessageFactory(
+    config,
+    configFile
+  );
+  const commitMessage = commitMessageFactory.create();
 
   // istanbul ignore if
   if (getAdminConfig().dryRun) {
@@ -57,6 +38,6 @@ export async function createOnboardingBranch(
         contents,
       },
     ],
-    message: commitMessage,
+    message: commitMessage.toString(),
   });
 }
diff --git a/lib/workers/repository/onboarding/branch/rebase.ts b/lib/workers/repository/onboarding/branch/rebase.ts
index 5080736251d3e8a4bce64b90fafdea811d76a066..82cd7d93fd7ca06e6a51f4f1877792118fd916ff 100644
--- a/lib/workers/repository/onboarding/branch/rebase.ts
+++ b/lib/workers/repository/onboarding/branch/rebase.ts
@@ -8,6 +8,7 @@ import {
   isBranchModified,
   isBranchStale,
 } from '../../../../util/git';
+import { OnboardingCommitMessageFactory } from './commit-message';
 import { getOnboardingConfigContents } from './config';
 
 const defaultConfigFile = (config: RenovateConfig): string =>
@@ -15,23 +16,6 @@ const defaultConfigFile = (config: RenovateConfig): string =>
     ? config.onboardingConfigFileName
     : configFileNames[0];
 
-function getCommitMessage(config: RenovateConfig): string {
-  const configFile = defaultConfigFile(config);
-  let commitMessage: string;
-  // istanbul ignore if
-  if (config.semanticCommits === 'enabled') {
-    commitMessage = config.semanticCommitType;
-    if (config.semanticCommitScope) {
-      commitMessage += `(${config.semanticCommitScope})`;
-    }
-    commitMessage += ': ';
-    commitMessage += 'add ' + configFile;
-  } else {
-    commitMessage = 'Add ' + configFile;
-  }
-  return commitMessage;
-}
-
 export async function rebaseOnboardingBranch(
   config: RenovateConfig
 ): Promise<string | null> {
@@ -52,7 +36,11 @@ export async function rebaseOnboardingBranch(
   }
   logger.debug('Rebasing onboarding branch');
   // istanbul ignore next
-  const commitMessage = getCommitMessage(config);
+  const commitMessageFactory = new OnboardingCommitMessageFactory(
+    config,
+    configFile
+  );
+  const commitMessage = commitMessageFactory.create();
 
   // istanbul ignore if
   if (getAdminConfig().dryRun) {
@@ -67,6 +55,6 @@ export async function rebaseOnboardingBranch(
         contents,
       },
     ],
-    message: commitMessage,
+    message: commitMessage.toString(),
   });
 }
diff --git a/lib/workers/repository/updates/generate.ts b/lib/workers/repository/updates/generate.ts
index e7e9d2b3adf14de782938b39612920195a5ca9e6..05ea64a7e548dbc396ee8fb2856779b00c05d258 100644
--- a/lib/workers/repository/updates/generate.ts
+++ b/lib/workers/repository/updates/generate.ts
@@ -7,7 +7,7 @@ import { logger } from '../../../logger';
 import { sanitize } from '../../../util/sanitize';
 import * as template from '../../../util/template';
 import type { BranchConfig, BranchUpgradeConfig } from '../../types';
-import { formatCommitMessagePrefix } from '../util/commit-message';
+import { CommitMessage } from '../model/commit-message';
 
 function isTypesGroup(branchUpgrades: BranchUpgradeConfig[]): boolean {
   return (
@@ -158,7 +158,7 @@ export function generateBranchConfig(
           upgrade
         )})`;
       }
-      upgrade.commitMessagePrefix = formatCommitMessagePrefix(semanticPrefix);
+      upgrade.commitMessagePrefix = CommitMessage.formatPrefix(semanticPrefix);
       upgrade.toLowerCase =
         // eslint-disable-next-line @typescript-eslint/prefer-regexp-exec
         upgrade.semanticCommitType.match(/[A-Z]/) === null &&
diff --git a/lib/workers/repository/util/commit-message.spec.ts b/lib/workers/repository/util/commit-message.spec.ts
deleted file mode 100644
index 6a263b014bb07f310455f0344bb8e35ca77767a4..0000000000000000000000000000000000000000
--- a/lib/workers/repository/util/commit-message.spec.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-import { getName } from '../../../../test/util';
-import {
-  COMMIT_MESSAGE_PREFIX_SEPARATOR,
-  formatCommitMessagePrefix,
-} from './commit-message';
-
-describe(getName(), () => {
-  describe('COMMIT_MESSAGE_PREFIX_END_CHARACTER', () => {
-    it('is a colon character', () => {
-      expect(COMMIT_MESSAGE_PREFIX_SEPARATOR).toBe(':');
-    });
-  });
-  describe('formatCommitMessagePrefix', () => {
-    it.each([
-      [
-        'adds a separator',
-        'does not end',
-        'RENOV-123',
-        `RENOV-123${COMMIT_MESSAGE_PREFIX_SEPARATOR}`,
-      ],
-      [
-        'does nothing',
-        'ends',
-        `RENOV-123${COMMIT_MESSAGE_PREFIX_SEPARATOR}`,
-        `RENOV-123${COMMIT_MESSAGE_PREFIX_SEPARATOR}`,
-      ],
-    ])(
-      '%s when the prefix %s with a separator',
-      (
-        expectedAction: string,
-        endingState: string,
-        commitMessagePrefix: string,
-        expectedPrefix: string
-      ) => {
-        expect(formatCommitMessagePrefix(commitMessagePrefix)).toBe(
-          expectedPrefix
-        );
-      }
-    );
-  });
-});
diff --git a/lib/workers/repository/util/commit-message.ts b/lib/workers/repository/util/commit-message.ts
deleted file mode 100644
index d84752f81a63b27c30063423470a29330ddebac2..0000000000000000000000000000000000000000
--- a/lib/workers/repository/util/commit-message.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-export const COMMIT_MESSAGE_PREFIX_SEPARATOR = ':';
-
-export const formatCommitMessagePrefix = (
-  commitMessagePrefix: string
-): string =>
-  `${commitMessagePrefix}${
-    commitMessagePrefix.endsWith(COMMIT_MESSAGE_PREFIX_SEPARATOR)
-      ? ''
-      : COMMIT_MESSAGE_PREFIX_SEPARATOR
-  }`;