diff --git a/create_debian-changelog-from-git.py b/create_debian-changelog-from-git.py
new file mode 100755
index 0000000000000000000000000000000000000000..61f5dde8358d26e7be2abf6f22b84edd3d5d93c9
--- /dev/null
+++ b/create_debian-changelog-from-git.py
@@ -0,0 +1,104 @@
+#!/usr/bin/python3
+import sys
+import re
+import textwrap
+import datetime
+import email.utils
+
+import git
+
+package_name = 'UNKNOWN'
+
+alias_names = {
+}
+
+ignore_commit_regexes = [
+	'^fixup!',
+]
+
+def print_release(tag=None, commits=tuple(), last_tag=None):
+	release_version = '0.0.0'
+	release_author = git.objects.util.Actor('None', 'undefined@example.com')
+	release_date = 0
+	release_status = 'UNRELEASED'
+	message = ''
+
+	if tag:
+		release_status = 'unstable'
+		release_version = tag.name[1:] # strip leading "v"
+		if isinstance(tag.object, git.TagObject):
+			release_author = tag.object.tagger
+			release_date = tag.object.tagged_date
+			message = tag.object.message.split('-----BEGIN PGP SIGNATURE-----')[0].strip()
+		else:
+			release_author = tag.object.committer
+			release_date = tag.object.committed_date
+	elif commits:
+		release_author = commits[0].committer
+		release_date = commits[0].committed_date
+		date = datetime.datetime.fromtimestamp(release_date).strftime('%Y%m%dT%H%M%S')
+		last_version = '0.0.0'
+		if last_tag:
+			last_version = last_tag.name[1:] # strip leading "v"
+		release_version = f'{last_version}+git{date}-{commits[0].hexsha[:8]}'
+
+	print(f'{package_name} ({release_version}) {release_status}; urgency=medium')
+	print()
+	if message:
+		print(textwrap.indent(message, '  '))
+		print()
+	commit_authors = [] # list of (key, author), sorted by first commit date
+	commit_author_emails = {} # author email -> key
+	commit_author_names = {} # author name -> key
+	commit_author_commits = {} # key -> list of commits
+	for commit in commits:
+		if any(filter(lambda pattern: re.match(pattern, commit.summary), ignore_commit_regexes)):
+			continue
+		if len(commit.parents) > 1:
+			continue # Ignore merge commits
+		author_name = alias_names.get(commit.author.name, commit.author.name)
+		key = commit_author_emails.get(commit.author.email)
+		if key is None:
+			key = commit_author_names.get(author_name)
+		if key is None:
+			key = commit.author.email
+			commit_authors.append((key, author_name))
+		commit_author_emails[commit.author.email] = key
+		commit_author_names[author_name] = key
+		commit_author_commits[key] = commit_author_commits.get(key, []) + [commit]
+	commit_authors.sort(key=lambda args: len(commit_author_commits[args[0]]))
+	for key, author_name in commit_authors:
+		print(f'  [ {author_name} ]')
+		for commit in commit_author_commits[key]:
+			lines = '\n'.join(textwrap.wrap(commit.summary, 90))
+			lines = '  * ' + textwrap.indent(lines, '    ').strip()
+			print(lines)
+		print()
+	print(f' -- {alias_names.get(release_author.name, release_author.name)} <{release_author.email}>  {email.utils.formatdate(release_date)}')
+
+if __name__ == '__main__':
+	repo = git.Repo('.')
+	package_name = sys.argv[1]
+
+	version_commits = {}
+	for tag in repo.tags:
+		if not re.fullmatch('v[0-9]+[.][0-9]+[.][0-9]+.*', tag.name):
+			continue
+		if isinstance(tag.object, git.TagObject):
+			commit_hexsha = tag.object.object.hexsha
+		else:
+			commit_hexsha = tag.object.hexsha
+		version_commits[commit_hexsha] = tag
+
+	tag = None
+	commits = []
+	for commit in repo.iter_commits('HEAD'):
+		if commit.hexsha in version_commits:
+			prev_tag = version_commits[commit.hexsha]
+			if commits:
+				print_release(tag, commits, last_tag=prev_tag)
+				print()
+			tag = prev_tag
+			commits = []
+		commits.append(commit)
+	print_release(tag, commits)
diff --git a/shared-ci.yml b/shared-ci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..0c4daffc9eecc02316b9212ee2c7ab2990077fce
--- /dev/null
+++ b/shared-ci.yml
@@ -0,0 +1,9 @@
+variables:
+  DEBIAN_FRONTEND: noninteractive 
+  GIT_SUBMODULE_STRATEGY: recursive
+  APT_API_URL: https://packages.cccv.de
+
+stages:
+- build
+- package
+- publish