From a82cfd286c4f820a3d8a4bf8e9f178de0f0a231c Mon Sep 17 00:00:00 2001
From: Julian Rother <julian@jrother.eu>
Date: Wed, 17 Nov 2021 14:57:03 +0100
Subject: [PATCH] Improved debian packaging and CI testing

Minimal supported python version is changed to 3.7 to support Debian Buster.
---
 .gitlab-ci.yml             | 183 +++++++++++++++++++++++++++++--------
 debian/changelog           |   5 -
 debian/control             |   7 +-
 debian/create_changelog.py | 106 +++++++++++++++++++++
 setup.py                   |   2 +-
 5 files changed, 254 insertions(+), 49 deletions(-)
 delete mode 100644 debian/changelog
 create mode 100755 debian/create_changelog.py

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index b91ce53..68af49f 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,8 +1,75 @@
-linter:
+image: registry.git.cccv.de/uffd/docker-images/buster
+
+variables:
+  DEBIAN_FRONTEND: noninteractive
+  GIT_SUBMODULE_STRATEGY: normal
+  PYTHONPATH: deps/ldapalchemy
+  APT_API_URL: https://packages.cccv.de
+  APT_REPO: uffd
+  PYLINT_PIN: pylint~=2.10.0
+
+before_script:
+  - python3 -V
+  - lsb_release -a
+  - uname -a
+  - python3 -m pylint --version
+  - python3 -m coverage --version
+  - echo "${CI_COMMIT_TAG}" | grep -qE "v[0-9]+[.][0-9]+[.][0-9]+.*" && export PACKAGE_VERSION="${CI_COMMIT_TAG#v}" || export PACKAGE_VERSION="${CI_COMMIT_SHA}"
+
+.build:
+  stage: build
+
+build:pip:
+  extends: .build
+  script:
+  - PACKAGE_VERSION="${PACKAGE_VERSION}" python3 -m build
+  artifacts:
+    paths:
+      - dist/*
+
+build:apt:
+  extends: .build
+  script:
+  - ./debian/create_changelog.py python3-ldapserver > debian/changelog
+  - dpkg-buildpackage -us -uc
+  - mv ../*.deb ./
+  - dpkg-deb -I *.deb
+  - dpkg-deb -c *.deb
+  artifacts:
+    paths:
+    - ./*.deb
+
+build:docs:
+  extends: .build
+  script:
+  - pip3 install Sphinx sphinx-rtd-theme
+  - cd docs && make html
+  artifacts:
+    paths:
+    - docs/_build/html/index.html
+    - docs/_build/html
+    expose_as: 'Documentation'
+
+linter:buster:
+  image: registry.git.cccv.de/uffd/docker-images/buster
+  stage: test
+  script:
+  - pip3 install $PYLINT_PIN pylint-gitlab
+  - python3 -m pylint --exit-zero --rcfile .pylintrc --output-format=pylint_gitlab.GitlabCodeClimateReporter ldapserver > codeclimate.json
+  - python3 -m pylint --exit-zero --rcfile .pylintrc --output-format=pylint_gitlab.GitlabPagesHtmlReporter ldapserver > pylint.html
+  - python3 -m pylint --rcfile .pylintrc --output-format=text ldapserver
+  artifacts:
+    when: always
+    paths:
+    - pylint.html
+    reports:
+      codequality: codeclimate.json
+
+linter:bullseye:
+  image: registry.git.cccv.de/uffd/docker-images/bullseye
   stage: test
-  image: python:3.7
   script:
-  - pip install pylint-gitlab
+  - pip3 install $PYLINT_PIN pylint-gitlab
   - python3 -m pylint --exit-zero --rcfile .pylintrc --output-format=pylint_gitlab.GitlabCodeClimateReporter ldapserver > codeclimate.json
   - python3 -m pylint --exit-zero --rcfile .pylintrc --output-format=pylint_gitlab.GitlabPagesHtmlReporter ldapserver > pylint.html
   - python3 -m pylint --rcfile .pylintrc --output-format=text ldapserver
@@ -10,19 +77,17 @@ linter:
     when: always
     paths:
     - pylint.html
-    expose_as: 'Linter Report'
     reports:
       codequality: codeclimate.json
 
-test:
+unittests:buster:
+  image: registry.git.cccv.de/uffd/docker-images/buster
   stage: test
-  image: python:3.7
   script:
-  - pip install pytest coverage
-  - coverage run --include 'ldapserver/*.py' -m pytest --junitxml=report.xml
-  - coverage report -m
-  - coverage html
-  - coverage xml
+  - python3-coverage run --include 'ldapserver/*.py' -m pytest --junitxml=report.xml || true
+  - python3-coverage report -m
+  - python3-coverage html
+  - python3-coverage xml
   artifacts:
     when: always
     paths:
@@ -34,40 +99,80 @@ test:
       junit: report.xml
   coverage: '/^TOTAL.*\s+(\d+\%)$/'
 
-build-docs:
-  stage: deploy
-  image: python:3.7
+unittests:bullseye:
+  image: registry.git.cccv.de/uffd/docker-images/bullseye
+  stage: test
   script:
-  - pip install Sphinx sphinx-rtd-theme
-  - cd docs && make html
+  - python3-coverage run --include 'ldapserver/*.py' -m pytest --junitxml=report.xml || true
+  #- python3-coverage report -m
+  - python3-coverage html
+  #- python3-coverage xml
   artifacts:
+    when: always
     paths:
-    - docs/_build/html/index.html
-    - docs/_build/html
-    expose_as: 'Documentation'
+    - htmlcov/index.html
+    - htmlcov
+    expose_as: 'Coverage Report'
+    reports:
+      #cobertura: coverage.xml
+      junit: report.xml
+  #coverage: '/^TOTAL.*\s+(\d+\%)$/'
 
-publish-pip:
-  stage: deploy
-  image: python:3.7
+test:package:pip:buster:
+  image: registry.git.cccv.de/uffd/docker-images/buster
+  stage: test
   script:
-  - pip install build twine
-  - PACKAGE_VERSION="${CI_COMMIT_TAG#v}" python -m build
-  - TWINE_USERNAME="${GITLABPKGS_USERNAME}" TWINE_PASSWORD="${GITLABPKGS_PASSWORD}" python -m twine upload --repository-url ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/pypi dist/*
-  - TWINE_USERNAME="${PYPI_USERNAME}" TWINE_PASSWORD="${PYPI_PASSWORD}" python -m twine upload dist/*
+  - pip3 install dist/*.tar.gz
+  dependencies:
+  - build:pip
+
+test:package:pip:bullseye:
+  image: registry.git.cccv.de/uffd/docker-images/bullseye
+  stage: test
+  script:
+  - pip3 install dist/*.tar.gz
+  dependencies:
+  - build:pip
+
+test:package:apt:buster:
+  image: registry.git.cccv.de/uffd/docker-images/buster
+  stage: test
+  script:
+  - apt -y install ./*.deb
+  dependencies:
+  - build:apt
+
+test:package:apt:bullseye:
+  image: registry.git.cccv.de/uffd/docker-images/bullseye
+  stage: test
+  script:
+  - apt -y install ./*.deb
+  dependencies:
+  - build:apt
+
+.publish:
+  stage: deploy
   rules:
     - if: '$CI_COMMIT_TAG =~ /v[0-9]+[.][0-9]+[.][0-9]+.*/'
 
-publish-apt:
-  image: debian:buster
-  stage: deploy
+publish:pip:
+  extends: .publish
   script:
-    - apt update
-    - apt-get install -y python3-all debhelper python3-pip git-buildpackage
-    - gbp dch --ignore-branch --debian-tag=v%\(version\)s
-    - dpkg-buildpackage -us -uc
-    - mkdir build
-    - mv ../*.deb build/
-  artifacts:
-    paths:
-      - build/*
-      - debian/changelog
+  - TWINE_USERNAME="${GITLABPKGS_USERNAME}" TWINE_PASSWORD="${GITLABPKGS_PASSWORD}" python3 -m twine upload --repository-url ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/pypi dist/*
+  - TWINE_USERNAME="${PYPI_USERNAME}" TWINE_PASSWORD="${PYPI_PASSWORD}" python3 -m twine upload dist/*
+  dependencies:
+  - build:pip
+
+publish:apt:
+  extends: .publish
+  script:
+  - export DEBPATH="$(echo *.deb)"
+  - echo Upload deb file, add it to repo and clean up upload
+  - curl --user "${APTLY_API_USER}:${APTLY_API_PW}" -X POST -F file=@"$DEBPATH" "${APT_API_URL}/api/files/${APT_REPO}-ci-upload-${CI_JOB_ID}"
+  - curl --user "${APTLY_API_USER}:${APTLY_API_PW}" -X POST "${APT_API_URL}/api/repos/${APT_REPO}/file/${APT_REPO}-ci-upload-${CI_JOB_ID}"
+  - curl --user "${APTLY_API_USER}:${APTLY_API_PW}" -X DELETE "${APT_API_URL}/api/files/${APT_REPO}-ci-upload-${CI_JOB_ID}"
+  - echo Update published repo for all distros
+  - 'curl --user "${APTLY_API_USER}:${APTLY_API_PW}" -X PUT -H "Content-Type: application/json" --data "{ }" "${APT_API_URL}/api/publish/uffd/buster"'
+  - 'curl --user "${APTLY_API_USER}:${APTLY_API_PW}" -X PUT -H "Content-Type: application/json" --data "{ }" "${APT_API_URL}/api/publish/uffd/bullseye"'
+  dependencies:
+  - build:apt
diff --git a/debian/changelog b/debian/changelog
deleted file mode 100644
index f8ae9c6..0000000
--- a/debian/changelog
+++ /dev/null
@@ -1,5 +0,0 @@
-python3-ldapserver (0.0.1.dev0) unstable; urgency=medium
-
-  * Initial release.
-
- -- Andreas Valder <nd@cccv.de>  Fri, 23 Jul 2021 18:02:31 +0200
diff --git a/debian/control b/debian/control
index aaab10c..cd146b7 100644
--- a/debian/control
+++ b/debian/control
@@ -1,7 +1,7 @@
 Source: python3-ldapserver
 Section: python
 Priority: optional
-Maintainer: Andreas Valder <nd@cccv.de>
+Maintainer: CCCV <it@cccv.de>
 Build-Depends:
  debhelper-compat (= 12),
  dh-python,
@@ -12,8 +12,7 @@ Homepage: https://git.cccv.de/uffd/python-ldapserver
 Vcs-Git: https://git.cccv.de/uffd/python-ldapserver.git
 
 Package: python3-ldapserver
-Architecture: any
+Architecture: all
 Depends:
  ${misc:Depends},
- ${python3:Depends},
-Description: LDAP server request handler
+Description: Library to implement special-purpose LDAP servers
diff --git a/debian/create_changelog.py b/debian/create_changelog.py
new file mode 100755
index 0000000..b62f211
--- /dev/null
+++ b/debian/create_changelog.py
@@ -0,0 +1,106 @@
+#!/usr/bin/python3
+import sys
+import re
+import textwrap
+import datetime
+import email.utils
+
+import git
+
+package_name = 'UNKNOWN'
+
+alias_names = {
+	'julian': 'Julian Rother',
+	'Julian': 'Julian Rother',
+}
+
+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/setup.py b/setup.py
index 46ebb45..63910cf 100644
--- a/setup.py
+++ b/setup.py
@@ -22,5 +22,5 @@ setuptools.setup(
 	],
 	package_dir={'': '.'},
 	packages=setuptools.find_packages(where='.'),
-	python_requires='>=3.9',
+	python_requires='>=3.7',
 )
-- 
GitLab