diff --git a/.gitignore b/.gitignore index 4cf8c91..eb25219 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ *~ .*~ +__pycache__/* diff --git a/Dockerfile b/Dockerfile index aad1291..6589228 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,15 +1,16 @@ -FROM debian:buster +FROM debian:bookworm LABEL Maintainer="Wolfgang Hottgenroth " -LABEL ImageName="registry.hottis.de/dockerized/base-build-env" LABEL AlternativeImageName="wollud1969/base-build-env" -ARG RELEASETOOL_URL="https://home.hottis.de/gitlab/wolutator/gitlabreleaseuploader/uploads/f97b219a1df1910822b9936a8236dfbd/GitlabReleaseTool.zip" +RUN \ + sed -i 's,deb.debian.org,ftp.de.debian.org,g' /etc/apt/sources.list.d/debian.sources RUN \ apt update && \ apt upgrade -y && \ + apt autoremove && \ apt install -y ca-certificates && \ apt install -y make && \ apt install -y openssh-client && \ @@ -20,28 +21,30 @@ RUN \ apt install -y python3-yaml && \ apt install -y python3-pip && \ apt install -y python3-xmltodict && \ + apt install -y python3-cheetah && \ apt install -y gpg && \ apt install -y apt-transport-https && \ apt install -y wget && \ apt install -y curl && \ apt install -y unzip && \ apt install -y zip && \ - apt install -y vim.tiny && \ + apt install -y vim-tiny && \ apt install -y p7zip-full && \ apt install -y procps && \ apt install -y doxygen && \ - apt install -y jq && \ - rm -rf /var/lib/apt/lists/* && \ - ln -s /usr/bin/python3 /usr/bin/python && \ - ln -s /usr/bin/pip3 /usr/bin/pip && \ - pip install Cheetah3 && \ - cd /tmp && \ - wget $RELEASETOOL_URL && \ - unzip GitlabReleaseTool.zip && \ - chmod 755 gitlabreleaseuploader.py && \ - chmod 755 deleterelease.py && \ - chmod 755 checksemver.py && \ - mv gitlabreleaseuploader.py /usr/bin && \ - mv deleterelease.py /usr/bin && \ - mv checksemver.py /usr/bin + apt install -y jq + +RUN \ + ln -s /usr/bin/python3 /usr/bin/python && \ + mkdir -p /tmp/gru + +COPY *.py /tmp/gru + +RUN \ + cd /tmp/gru && \ + for I in *.py; do python -m py_compile $I; done && \ + for I in *.py; do python -m pycodestyle --ignore=E501 $I; done && \ + chmod 755 *.py && \ + cp *.py /usr/bin + diff --git a/checksemver.py b/checksemver.py new file mode 100644 index 0000000..ebf5835 --- /dev/null +++ b/checksemver.py @@ -0,0 +1,80 @@ +#!/usr/bin/python + +import re +import argparse +import sys + +parser = argparse.ArgumentParser(description='Semantic Version Validator') +parser.add_argument('--versionToValidate', '-V', + help='The version to validate against the semantic versioning rules', + required=True) +parser.add_argument('--messageToValidate', '-M', + help='A message to validate, means: it must not be empty', + default='', + required=False) +parser.add_argument('--validateMessage', '-m', + help='Consider -M', + required=False, + action='store_true', + default=False) +parser.add_argument('--printExports', '-e', + help='Print exports', + action='store_true', + default=False, + required=False) +parser.add_argument('--exportFormat', '-f', + help='Print exports in >bash< or >powershell< format, to be used in ' + 'backticks or with Invoke-Expression', + default='bash', + required=False) +parser.add_argument('--verbose', '-v', + help='Verbose output, overrides -q', + required=False, + action='store_true', + default=False) +args = parser.parse_args() + +verbose = args.verbose +versionToValidate = args.versionToValidate +messageToValidate = args.messageToValidate +validateMessage = args.validateMessage +printExports = args.printExports +exportFormat = args.exportFormat + +r = re.compile(r'^(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)(?:-(?P(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$') + +if verbose: + print("Version to validate: {}".format(versionToValidate)) + +m = r.match(versionToValidate) + +if m: + if verbose: + for i in r.groupindex: + print("Found: {}: {}".format(i, m.group(i))) + if validateMessage: + if verbose: + print("Checking message {}".format(messageToValidate)) + if messageToValidate == '': + if verbose: + print("Message shall be validate and is invalid") + sys.exit(1) + else: + if printExports: + if exportFormat == "bash": + print("export MESSAGE={}".format(messageToValidate)) + elif exportFormat == "powershell": + print("set MESSAGE {}".format(messageToValidate)) + + if printExports: + for i in r.groupindex: + if exportFormat == "bash": + print("export {}={}".format(i.upper(), '' if m.group(i) is None else m.group(i))) + elif exportFormat == "powershell": + print("set {} {}".format(i.upper(), '""' if m.group(i) is None else m.group(i))) + + sys.exit(0) +else: + if verbose: + print("Version is invalid") + sys.exit(1) diff --git a/deleterelease.py b/deleterelease.py new file mode 100644 index 0000000..4bc89b9 --- /dev/null +++ b/deleterelease.py @@ -0,0 +1,66 @@ +#!/usr/bin/python + +import requests +import json +import argparse +import sys + + +parser = argparse.ArgumentParser(description='Gitlab Release Deleter') +parser.add_argument('--privateToken', '-p', + help='Private token to access Gitlab', required=True) +parser.add_argument('--projectId', '-i', + help='ProjectID of the related project', required=True) +parser.add_argument('--releaseTag', '-t', + help='Tag of the release in the repo', + required=True, + default='') +parser.add_argument('--instanceUrl', '-I', + help='URL of your gitlab instance', required=False, + default='https://gitlab.com') +parser.add_argument('--verbose', '-v', + help='verbose output', + required=False, + action='store_true', + default=False) +parser.add_argument('--caBundle', '-B', + help='File with the CA certificates to trust', required=False, + default='/etc/ssl/certs/ca-certificates.crt') +parser.add_argument('--insecure', + help='insecure ssl connect', + required=False, + action='store_true', + default=False) + + +args = parser.parse_args() + +privateToken = args.privateToken +projectId = args.projectId +releaseTag = args.releaseTag +instanceUrl = args.instanceUrl +verbose = args.verbose +caBundle = args.caBundle +insecure = args.insecure + +# --- delete release +deleteReleaseUrl = "%s/api/v4/projects/%s/releases/%s" % (instanceUrl, projectId, releaseTag) +headers = {"PRIVATE-TOKEN": privateToken, "Content-Type": "application/json"} + +if verbose: + print("URL: %s" % deleteReleaseUrl) + +if insecure: + caBundle = (False) + +deleteReleaseResult = requests.delete(deleteReleaseUrl, headers=headers, verify=caBundle) + +if deleteReleaseResult.status_code != 200: + print(deleteReleaseResult) + print(deleteReleaseResult.text) + raise Exception('Unable to delete release') + +if verbose: + print(deleteReleaseResult) + print(deleteReleaseResult.text) +print('Release successfully delete') diff --git a/gitlabreleaseuploader.py b/gitlabreleaseuploader.py new file mode 100755 index 0000000..69f9381 --- /dev/null +++ b/gitlabreleaseuploader.py @@ -0,0 +1,260 @@ +#!/usr/bin/python + +import requests +import json +import argparse +import sys + + +parser = argparse.ArgumentParser(description='Gitlab Release Uploader') +parser.add_argument('--privateToken', '-p', + help='Private token to access Gitlab', required=True) +parser.add_argument('--projectId', '-i', + help='ProjectID of the related project', required=True) +parser.add_argument('--projectUrl', '-u', + help='URL of the related project at Gitlab', required=True) +parser.add_argument('--file', '-f', + help='File to be released, can appear multiple times', + action='append', + required=False) +parser.add_argument('--releaseName', '-n', + help='Name of the release', required=False, + default='') +parser.add_argument('--createRelease', '-C', + help='Shall the release be created here', + required=False, + action='store_true', + default=False) +parser.add_argument('--releaseTag', '-t', + help='Tag of the release in the repo', + required=False, + default='') +parser.add_argument('--releaseTagTarget', '-T', + help='Commit or branch the tag should point to', + required=False, + default='') +parser.add_argument('--createReleaseTag', '-c', + help='Shall the release tag be created here', + required=False, + action='store_true', + default=False) +parser.add_argument('--description', '-d', + help='Description of the release', required=False, + default='') +parser.add_argument('--releaseInfoFile', '-F', + help='File containing JSON object with release info ' + '(release tag, create release tag, description', + required=False, + default='') +parser.add_argument('--instanceUrl', '-I', + help='URL of your gitlab instance', required=False, + default='https://gitlab.com') +parser.add_argument('--caBundle', '-B', + help='File with the CA certificates to trust', required=False, + default='/etc/ssl/certs/ca-certificates.crt') +parser.add_argument('--insecure', + help='insecure ssl connect', + required=False, + action='store_true', + default=False) +parser.add_argument('--verbose', '-v', + help='verbose output', + required=False, + action='store_true', + default=False) +args = parser.parse_args() + +privateToken = args.privateToken +projectId = args.projectId +projectUrl = args.projectUrl +filesToUpload = args.file +releaseName = args.releaseName +createRelease = args.createRelease +releaseTag = args.releaseTag +releaseTagTarget = args.releaseTagTarget +releaseDescription = args.description +instanceUrl = args.instanceUrl +createReleaseTag = args.createReleaseTag +releaseInfoFilename = args.releaseInfoFile +caBundle = args.caBundle +verbose = args.verbose +insecure = args.insecure + +releaseInfo = {} +if (releaseInfoFilename): + with open(releaseInfoFilename, 'r') as releaseInfoFile: + releaseInfo = releaseInfoFile.read() + releaseInfo = json.loads(releaseInfo) + if 'releaseName' in releaseInfo: + releaseName = releaseInfo['releaseName'] + if 'createRelease' in releaseInfo: + createRelease = (releaseInfo['createRelease'] in + ('true', 'True')) + if 'releaseTag' in releaseInfo: + releaseTag = releaseInfo['releaseTag'] + if 'releaseTagTarget' in releaseInfo: + releaseTagTarget = releaseInfo['releaseTagTarget'] + if 'createReleaseTag' in releaseInfo: + createReleaseTag = (releaseInfo['createReleaseTag'] in + ('true', 'True')) + if 'description' in releaseInfo: + releaseDescription = releaseInfo['description'] + +if releaseName == '': + raise Exception('No release name given') +if (releaseTag == '') and createRelease: + raise Exception('No release tag given but creation of release requested') +if (releaseTagTarget == '') and createReleaseTag: + raise Exception('No release tag target given but creation of tag requested') +if (releaseDescription == '') and createRelease: + raise Exception('No release description given but creation of release requested') + +if insecure: + caBundle = (False) + + +def checkAndShowResult(result, expectedCode, errorMessage): + global verbose + + if result.status_code != expectedCode: + print(result) + print(result.text) + raise Exception(errorMessage) + + if verbose: + print(result) + print(result.text) + + +# --- upload the file +assets = [] # is required later, must be defined +if filesToUpload: + url = "%s/api/v4/projects/%s/uploads" % (instanceUrl, projectId) + headers = {"PRIVATE-TOKEN": privateToken} + + for filename in filesToUpload: + with open(filename, 'rb') as filehandle: + files = {"file": filehandle} + + if verbose: + print("POST to {}".format(url)) + result = requests.post(url, files=files, headers=headers, verify=caBundle) + + checkAndShowResult(result, 201, 'Unable to upload file to Gitlab') + + assetUrl = projectUrl + json.loads(result.text)['url'] + assets.append({'name': filename, 'url': assetUrl, 'id': "new-link-{}".format(len(assets))}) + + print('File {} successfully uploaded, url is {}'.format(filename, assetUrl)) + +# --- create release tag +if createReleaseTag: + url = ("%s/api/v4//projects/%s/repository/tags" % (instanceUrl, projectId)) + headers = {"PRIVATE-TOKEN": privateToken, "Content-Type": "application/json"} + + payload = { + "tag_name": releaseTag, + "id": projectId, + "ref": releaseTagTarget, + "message": "Tag for release %s" % releaseName + } + + if verbose: + print("POST to {}".format(url)) + result = requests.post(url, headers=headers, data=json.dumps(payload), verify=caBundle) + + checkAndShowResult(result, 201, 'Unable to create release tag') + + print('Tag successfully created') + + +# --- create release +if createRelease: + url = "%s/api/v4/projects/%s/releases" % (instanceUrl, projectId) + headers = {"PRIVATE-TOKEN": privateToken, "Content-Type": "application/json"} + + payload = { + "name": releaseName, + "tag_name": releaseTag, + "description": releaseDescription + } + + if verbose: + print("POST to {}".format(url)) + result = requests.post(url, headers=headers, data=json.dumps(payload), verify=caBundle) + + checkAndShowResult(result, 201, 'Unable to create release') + + print('Release successfully created') + + +# --- update release in case of additional description +if not createRelease and releaseDescription: + # --- get release to fetch existing description + url = "%s/api/v4/projects/%s/releases/%s" % (instanceUrl, projectId, releaseName) + headers = {"PRIVATE-TOKEN": privateToken} + + if verbose: + print("GET to {}".format(url)) + result = requests.get(url, headers=headers, verify=caBundle) + + checkAndShowResult(result, 200, 'Unable to get release') + existingDescription = json.loads(result.text)['description'] + print("Existing description is {}".format(existingDescription)) + + releaseDescription += "\n\n---------------------------------------------\n\n" + releaseDescription += existingDescription + + # --- update release + url = "%s/api/v4/projects/%s/releases/%s" % (instanceUrl, projectId, releaseName) + headers = {"PRIVATE-TOKEN": privateToken, "Content-Type": "application/json"} + + payload = { + "name": releaseName, + "tag_name": releaseTag, + "description": releaseDescription + } + + if verbose: + print("PUT to {}".format(url)) + result = requests.put(url, headers=headers, data=json.dumps(payload), verify=caBundle) + + checkAndShowResult(result, 200, 'Unable to update release') + + print('Release successfully update') + + +# --- add assets +# get existing assets +headers = {"PRIVATE-TOKEN": privateToken} +url = "%s/api/v4/projects/%s/releases/%s" % (instanceUrl, projectId, releaseName) +if verbose: + print("GET to {}".format(url)) +result = requests.get(url, headers=headers, verify=caBundle) +checkAndShowResult(result, 200, 'Unable to get release information') + +# add existing assets to list of assets +links = json.loads(result.text)['assets']['links'] +assets.extend(links) + +# delete existing assets +headers = {"PRIVATE-TOKEN": privateToken} +for link in links: + assetId = link['id'] + url = "%s/api/v4/projects/%s/releases/%s/assets/links/%s" % (instanceUrl, projectId, releaseName, assetId) + if verbose: + print("DELETE to {}".format(url)) + result = requests.delete(url, headers=headers, verify=caBundle) + checkAndShowResult(result, 200, 'Unable to delete asset') + print("Asset {} successfully deleted".format(assetId)) + + +# create all assets ("existing" and new) +headers = {"PRIVATE-TOKEN": privateToken, "Content-Type": "application/json"} +url = "%s/api/v4/projects/%s/releases/%s/assets/links" % (instanceUrl, projectId, releaseName) +for asset in assets: + if verbose: + print("POST to {}".format(url)) + result = requests.post(url, headers=headers, data=json.dumps(asset), verify=caBundle) + checkAndShowResult(result, 201, "Unable to create asset") + print("Asset {} successfully created".format(asset))