diff --git a/docs/usage/configuration-options.md b/docs/usage/configuration-options.md
index 328c6be9515921048717f015ef62d21f45542597..b0fbe64344c200d2b8a49006aed4f615aee17b0b 100644
--- a/docs/usage/configuration-options.md
+++ b/docs/usage/configuration-options.md
@@ -2490,7 +2490,7 @@ Example setting source URL for package "dummy":
 
 <!-- prettier-ignore -->
 !!! note
-    Renovate can fetch changelogs from GitHub and GitLab platforms only, and setting the URL to an unsupported host/platform type won't change that.
+    Renovate can fetch changelogs from Bitbucket, Gitea (Forgejo), GitHub and GitLab platforms only, and setting the URL to an unsupported host/platform type won't change that.
 
 ### replacementName
 
diff --git a/lib/constants/platforms.ts b/lib/constants/platforms.ts
index 8ea80fefac8be38a29dfd7062e2deedc85a7fce5..4c7ef1fc515765d2a3556a5a85c27b9ecd9bb300 100644
--- a/lib/constants/platforms.ts
+++ b/lib/constants/platforms.ts
@@ -10,6 +10,7 @@ export type PlatformId =
 
 export const GITEA_API_USING_HOST_TYPES = [
   'gitea',
+  'gitea-changelog',
   'gitea-releases',
   'gitea-tags',
 ];
diff --git a/lib/modules/datasource/gitea-releases/schema.ts b/lib/modules/datasource/gitea-releases/schema.ts
index 93bd77ac21ecdad38ea3844eb93c5bec3c2564f5..fe57281c672d579e96b2769c2c57d68d4edb50b0 100644
--- a/lib/modules/datasource/gitea-releases/schema.ts
+++ b/lib/modules/datasource/gitea-releases/schema.ts
@@ -1,7 +1,9 @@
 import { z } from 'zod';
 
 export const ReleaseSchema = z.object({
+  name: z.string(),
   tag_name: z.string(),
+  body: z.string(),
   prerelease: z.boolean(),
   published_at: z.string().datetime({ offset: true }),
 });
diff --git a/lib/modules/platform/gitea/schema.ts b/lib/modules/platform/gitea/schema.ts
new file mode 100644
index 0000000000000000000000000000000000000000..13d1a60d4dbb22e31d7bd0b6805b1fdfbd34dfd4
--- /dev/null
+++ b/lib/modules/platform/gitea/schema.ts
@@ -0,0 +1,12 @@
+import { z } from 'zod';
+
+export const ContentsResponseSchema = z.object({
+  name: z.string(),
+  path: z.string(),
+  type: z.union([z.literal('file'), z.literal('dir')]),
+  content: z.string().nullable(),
+});
+
+export type ContentsResponse = z.infer<typeof ContentsResponseSchema>;
+
+export const ContentsListResponseSchema = z.array(ContentsResponseSchema);
diff --git a/lib/util/common.spec.ts b/lib/util/common.spec.ts
index 4ed8919ea95cc1211a3513815e0b6a4e4bddce73..8505ad27447467d948fedfb7c85387b18dce81c8 100644
--- a/lib/util/common.spec.ts
+++ b/lib/util/common.spec.ts
@@ -13,6 +13,8 @@ describe('util/common', () => {
       ${'https://myorg.visualstudio.com/my-project/_git/my-repo.git'}        | ${'azure'}
       ${'https://bitbucket.org/some-org/some-repo'}                          | ${'bitbucket'}
       ${'https://bitbucket.com/some-org/some-repo'}                          | ${'bitbucket'}
+      ${'https://gitea.com/semantic-release/gitlab'}                         | ${'gitea'}
+      ${'https://forgejo.example.com/semantic-release/gitlab'}               | ${'gitea'}
       ${'https://github.com/semantic-release/gitlab'}                        | ${'github'}
       ${'https://github-enterprise.example.com/chalk/chalk'}                 | ${'github'}
       ${'https://gitlab.com/chalk/chalk'}                                    | ${'gitlab'}
@@ -38,17 +40,24 @@ describe('util/common', () => {
         hostType: 'gitlab-changelog',
         matchHost: 'gl.example.com',
       });
+      hostRules.add({
+        hostType: 'unknown',
+        matchHost: 'f.example.com',
+      });
 
       expect(detectPlatform('https://bb.example.com/chalk/chalk')).toBe(
         'bitbucket'
       );
+      expect(detectPlatform('https://gt.example.com/chalk/chalk')).toBe(
+        'gitea'
+      );
       expect(detectPlatform('https://gh.example.com/chalk/chalk')).toBe(
         'github'
       );
       expect(detectPlatform('https://gl.example.com/chalk/chalk')).toBe(
         'gitlab'
       );
-      expect(detectPlatform('https://gt.example.com/chalk/chalk')).toBeNull();
+      expect(detectPlatform('https://f.example.com/chalk/chalk')).toBeNull();
     });
   });
 });
diff --git a/lib/util/common.ts b/lib/util/common.ts
index 7693906d657ad27fa55619ed3ffb0551cce42884..df7db5ab4ab45fa369583343bd3c52540732bc71 100644
--- a/lib/util/common.ts
+++ b/lib/util/common.ts
@@ -1,5 +1,6 @@
 import {
   BITBUCKET_API_USING_HOST_TYPES,
+  GITEA_API_USING_HOST_TYPES,
   GITHUB_API_USING_HOST_TYPES,
   GITLAB_API_USING_HOST_TYPES,
 } from '../constants';
@@ -14,7 +15,7 @@ import { parseUrl } from './url';
  */
 export function detectPlatform(
   url: string
-): 'azure' | 'bitbucket' | 'github' | 'gitlab' | null {
+): 'azure' | 'bitbucket' | 'gitea' | 'github' | 'gitlab' | null {
   const { hostname } = parseUrl(url) ?? {};
   if (hostname === 'dev.azure.com' || hostname?.endsWith('.visualstudio.com')) {
     return 'azure';
@@ -22,6 +23,14 @@ export function detectPlatform(
   if (hostname === 'bitbucket.org' || hostname?.includes('bitbucket')) {
     return 'bitbucket';
   }
+  if (
+    hostname &&
+    (['gitea.com', 'codeberg.org'].includes(hostname) ||
+      hostname.includes('gitea') ||
+      hostname.includes('forgejo'))
+  ) {
+    return 'gitea';
+  }
   if (hostname === 'github.com' || hostname?.includes('github')) {
     return 'github';
   }
@@ -38,6 +47,9 @@ export function detectPlatform(
   if (BITBUCKET_API_USING_HOST_TYPES.includes(hostType)) {
     return 'bitbucket';
   }
+  if (GITEA_API_USING_HOST_TYPES.includes(hostType)) {
+    return 'gitea';
+  }
   if (GITHUB_API_USING_HOST_TYPES.includes(hostType)) {
     return 'github';
   }
diff --git a/lib/workers/repository/update/pr/changelog/api.ts b/lib/workers/repository/update/pr/changelog/api.ts
index 3abff281931f370eee073d16364809f5b810ecaf..d4d0c9b9b17ce044b21e116b5a7fb948c61a399c 100644
--- a/lib/workers/repository/update/pr/changelog/api.ts
+++ b/lib/workers/repository/update/pr/changelog/api.ts
@@ -1,4 +1,5 @@
 import { BitbucketChangeLogSource } from './bitbucket/source';
+import { GiteaChangeLogSource } from './gitea/source';
 import { GitHubChangeLogSource } from './github/source';
 import { GitLabChangeLogSource } from './gitlab/source';
 import type { ChangeLogSource } from './source';
@@ -7,5 +8,6 @@ const api = new Map<string, ChangeLogSource>();
 export default api;
 
 api.set('bitbucket', new BitbucketChangeLogSource());
+api.set('gitea', new GiteaChangeLogSource());
 api.set('github', new GitHubChangeLogSource());
 api.set('gitlab', new GitLabChangeLogSource());
diff --git a/lib/workers/repository/update/pr/changelog/gitea/index.spec.ts b/lib/workers/repository/update/pr/changelog/gitea/index.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..bff07ba853a1dcf96a5d04e14989ca51461591a0
--- /dev/null
+++ b/lib/workers/repository/update/pr/changelog/gitea/index.spec.ts
@@ -0,0 +1,497 @@
+import { getChangeLogJSON } from '..';
+import * as httpMock from '../../../../../../../test/http-mock';
+import { partial } from '../../../../../../../test/util';
+import * as semverVersioning from '../../../../../../modules/versioning/semver';
+import * as hostRules from '../../../../../../util/host-rules';
+import { toBase64 } from '../../../../../../util/string';
+import type { BranchUpgradeConfig } from '../../../../../types';
+import { GiteaChangeLogSource } from '../gitea/source';
+import { getReleaseNotesMd } from '.';
+
+const upgrade = partial<BranchUpgradeConfig>({
+  manager: 'some-manager',
+  branchName: '',
+  endpoint: 'https://gitea.com/api/v1/',
+  packageName: 'renovate',
+  versioning: semverVersioning.id,
+  currentVersion: '5.2.0',
+  newVersion: '5.7.0',
+  sourceUrl: 'https://gitea.com/meno/dropzone/',
+  releases: [
+    // TODO: test gitRef
+    { version: '5.2.0' },
+    {
+      version: '5.4.0',
+      releaseTimestamp: '2018-08-24T14:23:00.000Z',
+    },
+    { version: '5.5.0', gitRef: 'eba303e91c930292198b2fc57040145682162a1b' },
+    { version: '5.6.0', releaseTimestamp: '2020-02-13T15:37:00.000Z' },
+    { version: '5.6.1' },
+  ],
+});
+
+const matchHost = 'https://gitea.com/';
+
+const changelogSource = new GiteaChangeLogSource();
+
+describe('workers/repository/update/pr/changelog/gitea/index', () => {
+  beforeAll(() => {
+    // TODO: why?
+    delete process.env.GITHUB_ENDPOINT;
+  });
+
+  describe('getChangeLogJSON', () => {
+    beforeEach(() => {
+      hostRules.clear();
+      hostRules.add({
+        hostType: 'gitea',
+        matchHost,
+        token: 'abc',
+      });
+    });
+
+    it('returns null if @types', async () => {
+      expect(
+        await getChangeLogJSON({
+          ...upgrade,
+          currentVersion: undefined,
+        })
+      ).toBeNull();
+    });
+
+    it('returns null if currentVersion equals newVersion', async () => {
+      expect(
+        await getChangeLogJSON({
+          ...upgrade,
+          currentVersion: '1.0.0',
+          newVersion: '1.0.0',
+        })
+      ).toBeNull();
+    });
+
+    it('skips invalid repos', async () => {
+      expect(
+        await getChangeLogJSON({
+          ...upgrade,
+          sourceUrl: 'https://gitea.com/help',
+        })
+      ).toBeNull();
+    });
+
+    it('works without gitea', async () => {
+      expect(
+        await getChangeLogJSON({
+          ...upgrade,
+        })
+      ).toMatchObject({
+        hasReleaseNotes: false,
+        project: {
+          apiBaseUrl: 'https://gitea.com/api/v1/',
+          baseUrl: 'https://gitea.com/',
+          packageName: 'renovate',
+          repository: 'meno/dropzone',
+          sourceDirectory: undefined,
+          sourceUrl: 'https://gitea.com/meno/dropzone/',
+          type: 'gitea',
+        },
+        versions: [
+          { version: '5.6.1' },
+          { version: '5.6.0' },
+          { version: '5.5.0' },
+          { version: '5.4.0' },
+        ],
+      });
+      // TODO: find right mocks
+      httpMock.clear(false);
+    });
+
+    it('uses gitea tags', async () => {
+      httpMock
+        .scope(matchHost)
+        .get('/api/v1/repos/meno/dropzone/tags')
+        .times(8)
+        .reply(200, [
+          {
+            name: 'v5.2.0',
+            commit: { sha: 'abc', created: '2023-07-27T06:19:02Z' },
+          },
+          {
+            name: 'v5.4.0',
+            commit: { sha: 'abc', created: '2023-07-27T06:19:02Z' },
+          },
+          {
+            name: 'v5.5.0',
+            commit: { sha: 'abc', created: '2023-07-27T06:19:02Z' },
+          },
+          {
+            name: 'v5.6.0',
+            commit: { sha: 'abc', created: '2023-07-27T06:19:02Z' },
+          },
+          {
+            name: 'v5.6.1',
+            commit: { sha: 'abc', created: '2023-07-27T06:19:02Z' },
+          },
+          {
+            name: 'v5.7.0',
+            commit: { sha: 'abc', created: '2023-07-27T06:19:02Z' },
+          },
+        ])
+        .get('/api/v1/repos/meno/dropzone/contents')
+        .times(4)
+        .reply(200, [])
+        .get('/api/v1/repos/meno/dropzone/releases?draft=false')
+        .times(4)
+        .reply(200, [
+          {
+            name: 'v5.2.0',
+            tag_name: 'v5.2.0',
+            body: '',
+            prerelease: false,
+            published_at: '2023-07-27T06:19:02Z',
+          },
+          {
+            name: 'v5.4.0',
+            tag_name: 'v5.4.0',
+            body: '',
+            prerelease: false,
+            published_at: '2023-07-27T06:19:02Z',
+          },
+          {
+            name: 'v5.5.0',
+            tag_name: 'v5.5.0',
+            body: '',
+            prerelease: false,
+            published_at: '2023-07-27T06:19:02Z',
+          },
+          {
+            name: 'v5.6.0',
+            tag_name: 'v5.6.0',
+            body: '',
+            prerelease: false,
+            published_at: '2023-07-27T06:19:02Z',
+          },
+          {
+            name: '5.6.1 - Some feature',
+            tag_name: 'v5.6.1',
+            body: 'some changes',
+            prerelease: false,
+            published_at: '2023-07-27T06:19:02Z',
+          },
+          {
+            name: 'v5.7.0',
+            tag_name: 'v5.7.0',
+            body: '',
+            prerelease: false,
+            published_at: '2023-07-27T06:19:02Z',
+          },
+        ]);
+      expect(
+        await getChangeLogJSON({
+          ...upgrade,
+        })
+      ).toMatchObject({
+        hasReleaseNotes: true,
+        project: {
+          apiBaseUrl: 'https://gitea.com/api/v1/',
+          baseUrl: 'https://gitea.com/',
+          packageName: 'renovate',
+          repository: 'meno/dropzone',
+          sourceDirectory: undefined,
+          sourceUrl: 'https://gitea.com/meno/dropzone/',
+          type: 'gitea',
+        },
+        versions: [
+          {
+            version: '5.6.1',
+            releaseNotes: {
+              body: 'some changes\n',
+              name: '5.6.1 - Some feature',
+              notesSourceUrl:
+                'https://gitea.com/api/v1/repos/meno/dropzone/releases',
+              tag: 'v5.6.1',
+              url: 'https://gitea.com/api/v1/repos/meno/dropzone/releases/tag/v5.6.1',
+            },
+          },
+          { version: '5.6.0' },
+          { version: '5.5.0' },
+          { version: '5.4.0' },
+        ],
+      });
+    });
+
+    it('handles empty gitea tags response', async () => {
+      httpMock
+        .scope(matchHost)
+        .get('/api/v1/repos/meno/dropzone/tags')
+        .times(8)
+        .reply(200, [])
+        .get('/api/v1/repos/meno/dropzone/contents')
+        .times(4)
+        .reply(200, [])
+        .get('/api/v1/repos/meno/dropzone/releases?draft=false')
+        .times(4)
+        .reply(200, []);
+      expect(
+        await getChangeLogJSON({
+          ...upgrade,
+        })
+      ).toMatchObject({
+        hasReleaseNotes: false,
+        project: {
+          apiBaseUrl: 'https://gitea.com/api/v1/',
+          baseUrl: 'https://gitea.com/',
+          packageName: 'renovate',
+          repository: 'meno/dropzone',
+          sourceDirectory: undefined,
+          sourceUrl: 'https://gitea.com/meno/dropzone/',
+          type: 'gitea',
+        },
+        versions: [
+          { version: '5.6.1' },
+          { version: '5.6.0' },
+          { version: '5.5.0' },
+          { version: '5.4.0' },
+        ],
+      });
+    });
+
+    it('uses gitea tags with error', async () => {
+      httpMock
+        .scope(matchHost)
+        .get('/api/v1/repos/meno/dropzone/tags')
+        .times(8)
+        .replyWithError('Unknown gitea Repo')
+        .get('/api/v1/repos/meno/dropzone/contents')
+        .times(4)
+        .reply(200, [])
+        .get('/api/v1/repos/meno/dropzone/releases?draft=false')
+        .times(4)
+        .reply(200, []);
+      expect(
+        await getChangeLogJSON({
+          ...upgrade,
+        })
+      ).toMatchObject({
+        hasReleaseNotes: false,
+        project: {
+          apiBaseUrl: 'https://gitea.com/api/v1/',
+          baseUrl: 'https://gitea.com/',
+          packageName: 'renovate',
+          repository: 'meno/dropzone',
+          sourceDirectory: undefined,
+          sourceUrl: 'https://gitea.com/meno/dropzone/',
+          type: 'gitea',
+        },
+        versions: [
+          { version: '5.6.1' },
+          { version: '5.6.0' },
+          { version: '5.5.0' },
+          { version: '5.4.0' },
+        ],
+      });
+    });
+
+    it('handles no sourceUrl', async () => {
+      expect(
+        await getChangeLogJSON({
+          ...upgrade,
+          sourceUrl: undefined,
+        })
+      ).toBeNull();
+    });
+
+    it('handles invalid sourceUrl', async () => {
+      expect(
+        await getChangeLogJSON({
+          ...upgrade,
+          sourceUrl: 'http://example.com',
+        })
+      ).toBeNull();
+    });
+
+    it('handles no releases', async () => {
+      expect(
+        await getChangeLogJSON({
+          ...upgrade,
+          releases: [],
+        })
+      ).toBeNull();
+    });
+
+    it('handles not enough releases', async () => {
+      expect(
+        await getChangeLogJSON({
+          ...upgrade,
+          releases: [{ version: '0.9.0' }],
+        })
+      ).toBeNull();
+    });
+
+    it('supports gitea enterprise and gitea enterprise changelog', async () => {
+      hostRules.add({
+        hostType: 'gitea',
+        matchHost: 'https://gitea-enterprise.example.com/',
+        token: 'abc',
+      });
+      expect(
+        await getChangeLogJSON({
+          ...upgrade,
+          sourceUrl: 'https://gitea-enterprise.example.com/meno/dropzone/',
+          endpoint: 'https://gitea-enterprise.example.com/',
+        })
+      ).toMatchObject({
+        hasReleaseNotes: false,
+        project: {
+          apiBaseUrl: 'https://gitea-enterprise.example.com/api/v1/',
+          baseUrl: 'https://gitea-enterprise.example.com/',
+          packageName: 'renovate',
+          repository: 'meno/dropzone',
+          sourceDirectory: undefined,
+          sourceUrl: 'https://gitea-enterprise.example.com/meno/dropzone/',
+          type: 'gitea',
+        },
+        versions: [
+          { version: '5.6.1' },
+          { version: '5.6.0' },
+          { version: '5.5.0' },
+          { version: '5.4.0' },
+        ],
+      });
+
+      // TODO: find right mocks
+      httpMock.clear(false);
+    });
+
+    it('supports self-hosted gitea changelog', async () => {
+      httpMock.scope('https://git.test.com').persist().get(/.*/).reply(200, []);
+      hostRules.add({
+        hostType: 'gitea',
+        matchHost: 'https://git.test.com/',
+        token: 'abc',
+      });
+      expect(
+        await getChangeLogJSON({
+          ...upgrade,
+          platform: 'gitea',
+          sourceUrl: 'https://git.test.com/meno/dropzone/',
+          endpoint: 'https://git.test.com/api/v1/',
+        })
+      ).toMatchObject({
+        hasReleaseNotes: false,
+        project: {
+          apiBaseUrl: 'https://git.test.com/api/v1/',
+          baseUrl: 'https://git.test.com/',
+          packageName: 'renovate',
+          repository: 'meno/dropzone',
+          sourceDirectory: undefined,
+          sourceUrl: 'https://git.test.com/meno/dropzone/',
+          type: 'gitea',
+        },
+        versions: [
+          { version: '5.6.1' },
+          { version: '5.6.0' },
+          { version: '5.5.0' },
+          { version: '5.4.0' },
+        ],
+      });
+
+      // TODO: find right mocks
+      httpMock.clear(false);
+    });
+
+    it('supports overwriting sourceUrl for self-hosted gitea changelog', async () => {
+      httpMock.scope('https://git.test.com').persist().get(/.*/).reply(200, []);
+      const sourceUrl = 'https://git.test.com/meno/dropzone/';
+      const replacementSourceUrl =
+        'https://git.test.com/replacement/sourceurl/';
+      const config = {
+        ...upgrade,
+        platform: 'gitea',
+        endpoint: 'https://git.test.com/api/v1/',
+        sourceUrl,
+        customChangelogUrl: replacementSourceUrl,
+      };
+      hostRules.add({
+        hostType: 'gitea',
+        matchHost: 'https://git.test.com/',
+        token: 'abc',
+      });
+      expect(await getChangeLogJSON(config)).toMatchObject({
+        hasReleaseNotes: false,
+        project: {
+          apiBaseUrl: 'https://git.test.com/api/v1/',
+          baseUrl: 'https://git.test.com/',
+          packageName: 'renovate',
+          repository: 'replacement/sourceurl',
+          sourceDirectory: undefined,
+          sourceUrl: 'https://git.test.com/replacement/sourceurl/',
+          type: 'gitea',
+        },
+      });
+      expect(config.sourceUrl).toBe(sourceUrl); // ensure unmodified function argument
+
+      // TODO: find right mocks
+      httpMock.clear(false);
+    });
+  });
+
+  describe('hasValidRepository', () => {
+    it('handles invalid repository', () => {
+      expect(changelogSource.hasValidRepository('foo')).toBeFalse();
+      expect(changelogSource.hasValidRepository('some/repo/name')).toBeFalse();
+    });
+
+    it('handles valid repository', () => {
+      expect(changelogSource.hasValidRepository('some/repo')).toBeTrue();
+    });
+  });
+
+  describe('getAllTags', () => {
+    it('handles endpoint', async () => {
+      httpMock
+        .scope('https://git.test.com/')
+        .get('/api/v1/repos/some/repo/tags')
+        .reply(200, [
+          { name: 'v5.2.0' },
+          { name: 'v5.4.0' },
+          { name: 'v5.5.0' },
+        ]);
+      expect(
+        await changelogSource.getAllTags('https://git.test.com/', 'some/repo')
+      ).toEqual([]);
+    });
+  });
+
+  describe('getReleaseNotesMd', () => {
+    it('works', async () => {
+      httpMock
+        .scope('https://git.test.com/')
+        .get('/api/v1/repos/some/repo/contents/charts/some')
+        .reply(200, [
+          {
+            name: 'CHANGELOG.md',
+            path: 'charts/some/CHANGELOG.md',
+            type: 'file',
+            content: null,
+          },
+        ])
+        .get('/api/v1/repos/some/repo/contents/charts/some/CHANGELOG.md')
+        .reply(200, {
+          name: 'CHANGELOG.md',
+          path: 'charts/some/CHANGELOG.md',
+          type: 'file',
+          content: toBase64('some content'),
+        });
+      expect(
+        await getReleaseNotesMd(
+          'some/repo',
+          'https://git.test.com/api/v1/',
+          'charts/some'
+        )
+      ).toEqual({
+        changelogFile: 'charts/some/CHANGELOG.md',
+        changelogMd: 'some content\n#\n##',
+      });
+    });
+  });
+});
diff --git a/lib/workers/repository/update/pr/changelog/gitea/index.ts b/lib/workers/repository/update/pr/changelog/gitea/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..8f354fe6f1712a30c61be45c59c3ae3963978f6b
--- /dev/null
+++ b/lib/workers/repository/update/pr/changelog/gitea/index.ts
@@ -0,0 +1,92 @@
+import changelogFilenameRegex from 'changelog-filename-regex';
+import { logger } from '../../../../../../logger';
+import { ReleasesSchema } from '../../../../../../modules/datasource/gitea-releases/schema';
+import {
+  ContentsListResponseSchema,
+  ContentsResponse,
+  ContentsResponseSchema,
+} from '../../../../../../modules/platform/gitea/schema';
+import { GiteaHttp } from '../../../../../../util/http/gitea';
+import { fromBase64 } from '../../../../../../util/string';
+import type {
+  ChangeLogFile,
+  ChangeLogNotes,
+  ChangeLogProject,
+  ChangeLogRelease,
+} from '../types';
+
+export const id = 'gitea-changelog';
+const http = new GiteaHttp(id);
+
+export async function getReleaseNotesMd(
+  repository: string,
+  apiBaseUrl: string,
+  sourceDirectory?: string
+): Promise<ChangeLogFile | null> {
+  logger.trace('gitea.getReleaseNotesMd()');
+  const apiPrefix = `${apiBaseUrl}repos/${repository}/contents`;
+
+  const sourceDir = sourceDirectory ? `/${sourceDirectory}` : '';
+  const tree = (
+    await http.getJson(
+      `${apiPrefix}${sourceDir}`,
+      {
+        paginate: false, // no pagination yet
+      },
+      ContentsListResponseSchema
+    )
+  ).body;
+  const allFiles = tree.filter((f) => f.type === 'file');
+  let files: ContentsResponse[] = [];
+  if (!files.length) {
+    files = allFiles.filter((f) => changelogFilenameRegex.test(f.name));
+  }
+  if (!files.length) {
+    logger.trace('no changelog file found');
+    return null;
+  }
+
+  const { path: changelogFile } = files.shift()!;
+  /* istanbul ignore if */
+  if (files.length !== 0) {
+    logger.debug(
+      `Multiple candidates for changelog file, using ${changelogFile}`
+    );
+  }
+
+  const fileRes = await http.getJson(
+    `${apiPrefix}/${changelogFile}`,
+    ContentsResponseSchema
+  );
+  // istanbul ignore if: should never happen
+  if (!fileRes.body.content) {
+    logger.debug(`Missing content for changelog file, using ${changelogFile}`);
+    return null;
+  }
+  const changelogMd = fromBase64(fileRes.body.content) + '\n#\n##';
+
+  return { changelogFile, changelogMd };
+}
+
+export async function getReleaseList(
+  project: ChangeLogProject,
+  _release: ChangeLogRelease
+): Promise<ChangeLogNotes[]> {
+  logger.trace('gitea.getReleaseNotesMd()');
+  const apiUrl = `${project.apiBaseUrl}repos/${project.repository}/releases`;
+
+  const res = await http.getJson(
+    `${apiUrl}?draft=false`,
+    {
+      paginate: true,
+    },
+    ReleasesSchema
+  );
+  return res.body.map((release) => ({
+    url: `${apiUrl}/tag/${release.tag_name}`,
+    notesSourceUrl: apiUrl,
+    name: release.name,
+    body: release.body,
+    tag: release.tag_name,
+  }));
+}
diff --git a/lib/workers/repository/update/pr/changelog/gitea/source.ts b/lib/workers/repository/update/pr/changelog/gitea/source.ts
new file mode 100644
index 0000000000000000000000000000000000000000..fd78892d78232728404c12502301093d1bd4b319
--- /dev/null
+++ b/lib/workers/repository/update/pr/changelog/gitea/source.ts
@@ -0,0 +1,25 @@
+import type { BranchUpgradeConfig } from '../../../../../types';
+import { ChangeLogSource } from '../source';
+
+export class GiteaChangeLogSource extends ChangeLogSource {
+  constructor() {
+    super('gitea', 'gitea-tags');
+  }
+
+  getAPIBaseUrl(config: BranchUpgradeConfig): string {
+    return this.getBaseUrl(config) + 'api/v1/';
+  }
+
+  getCompareURL(
+    baseUrl: string,
+    repository: string,
+    prevHead: string,
+    nextHead: string
+  ): string {
+    return `${baseUrl}${repository}/compare/${prevHead}...${nextHead}`;
+  }
+
+  override hasValidRepository(repository: string): boolean {
+    return repository.split('/').length === 2;
+  }
+}
diff --git a/lib/workers/repository/update/pr/changelog/release-notes.ts b/lib/workers/repository/update/pr/changelog/release-notes.ts
index caecaf207110732f39ba26167083631e14c86ee6..b2c4117e2d397a4bb0753804579361d6d52b734f 100644
--- a/lib/workers/repository/update/pr/changelog/release-notes.ts
+++ b/lib/workers/repository/update/pr/changelog/release-notes.ts
@@ -10,6 +10,7 @@ import { newlineRegex, regEx } from '../../../../../util/regex';
 import { validateUrl } from '../../../../../util/url';
 import type { BranchUpgradeConfig } from '../../../../types';
 import * as bitbucket from './bitbucket';
+import * as gitea from './gitea';
 import * as github from './github';
 import * as gitlab from './gitlab';
 import type {
@@ -33,6 +34,8 @@ export async function getReleaseList(
   const { apiBaseUrl, repository, type } = project;
   try {
     switch (type) {
+      case 'gitea':
+        return await gitea.getReleaseList(project, release);
       case 'gitlab':
         return await gitlab.getReleaseList(project, release);
       case 'github':
@@ -242,6 +245,12 @@ export async function getReleaseNotesMdFileInner(
   const sourceDirectory = project.sourceDirectory!;
   try {
     switch (type) {
+      case 'gitea':
+        return await gitea.getReleaseNotesMd(
+          repository,
+          apiBaseUrl,
+          sourceDirectory
+        );
       case 'gitlab':
         return await gitlab.getReleaseNotesMd(
           repository,
diff --git a/lib/workers/repository/update/pr/changelog/source.ts b/lib/workers/repository/update/pr/changelog/source.ts
index 2f35b16dd7c773fa9ae3549e68997232bc2b652d..6855f1b3ad58b01fd7e7e2e50d34055e662d91c8 100644
--- a/lib/workers/repository/update/pr/changelog/source.ts
+++ b/lib/workers/repository/update/pr/changelog/source.ts
@@ -12,21 +12,22 @@ import { addReleaseNotes } from './release-notes';
 import { getInRangeReleases } from './releases';
 import type {
   ChangeLogError,
+  ChangeLogPlatform,
   ChangeLogRelease,
   ChangeLogResult,
 } from './types';
 
 export abstract class ChangeLogSource {
-  private platform;
-  private datasource;
-  private cacheNamespace: string;
+  private readonly cacheNamespace: string;
 
   constructor(
-    platform: 'bitbucket' | 'github' | 'gitlab',
-    datasource: 'bitbucket-tags' | 'github-tags' | 'gitlab-tags'
+    private readonly platform: ChangeLogPlatform,
+    private readonly datasource:
+      | 'bitbucket-tags'
+      | 'gitea-tags'
+      | 'github-tags'
+      | 'gitlab-tags'
   ) {
-    this.platform = platform;
-    this.datasource = datasource;
     this.cacheNamespace = `changelog-${platform}-release`;
   }
 
diff --git a/lib/workers/repository/update/pr/changelog/types.ts b/lib/workers/repository/update/pr/changelog/types.ts
index 34f4d3593942579f55883de215aa4594c1201cec..4c14a6c83af56f99b9d54e71815ef3b6cc0052be 100644
--- a/lib/workers/repository/update/pr/changelog/types.ts
+++ b/lib/workers/repository/update/pr/changelog/types.ts
@@ -23,9 +23,11 @@ export interface ChangeLogRelease {
   gitRef: string;
 }
 
+export type ChangeLogPlatform = 'bitbucket' | 'gitea' | 'github' | 'gitlab';
+
 export interface ChangeLogProject {
   packageName?: string;
-  type: 'bitbucket' | 'github' | 'gitlab';
+  type: ChangeLogPlatform;
   apiBaseUrl?: string;
   baseUrl: string;
   repository: string;