Skip to content

Commit 67a3e96

Browse files
author
Marc Rooding
committed
Initial version
0 parents  commit 67a3e96

File tree

4 files changed

+211
-0
lines changed

4 files changed

+211
-0
lines changed

Dockerfile

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
FROM python:3.7
2+
3+
RUN apt-get -y upgrade && \
4+
apt-get -y install git && \
5+
apt-get clean
6+
7+
WORKDIR /version-update
8+
9+
COPY /requirements.txt .
10+
11+
RUN pip install -r requirements.txt
12+
13+
COPY /version-update.py .
14+
15+
CMD ["python", "/version-update.py"]

README.md

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
# gitlab-semantic-versioning
2+
3+
Docker image that can be used to automatically version projects using semantic versioning.
4+
5+
Visit [semver.org](https://semver.org/) to read more about semantic versioning.
6+
7+
## How is the version determined?
8+
9+
Versions are being maintained using git tags.
10+
11+
If no git tag is available, the first version update will result in version 1.0.0.
12+
If git tags are available, it will determine whether to do a major, minor, or patch update based on specific merge request labels. The `bump-minor` and `bump-major` labels exist to do either a minor or major bump. If a merge request has no labels attached, it will perform a patch update by default.
13+
14+
## Prerequisites
15+
16+
### Group labels
17+
18+
As stated above, the version update workflow relies on merge request labels to determine the new version. The `bump-minor` and `bump-major` labels have been set as global GitLab labels. However, global labels only propogate to groups created after setting a global label. When adding a global label, they [do not automatically propogate to existing groups](https://gitlab.com/gitlab-org/gitlab-ce/issues/12707).
19+
20+
If you cannot select the specified labels in your merge request, your group was most likely created before the global labels were defined. Please follow [this guide to setup group-specific labels](https://docs.gitlab.com/ee/user/project/labels.html)
21+
22+
### API token and group
23+
24+
To extract the labels from merge requests, we need an API token to access the Gitlab API. Unfortunately, [GitLab doesn't yet support non-user specific access tokens](https://gitlab.com/gitlab-org/gitlab-ee/issues/756).
25+
26+
Ask your GitLab administrator to add a dummy user `${group_name}_npa` to GitLab with access only to your project group. Log in with this user, and create a [personal access token](https://gitlab.wbaa.pl.ing.net/profile/personal_access_tokens) with api scope access.
27+
28+
Copy the generated API token and keep it available for the next section.
29+
30+
### Group-level variables
31+
32+
The NPA username and token need to be injected into the version-update container as environment variables. For this, we'll use group-level variables.
33+
34+
Go to your group's variables section under `Settings` -> `CI / CD`.
35+
36+
Add the following variables:
37+
38+
| Key | Value |
39+
|-----------------|----------------------------------------------------------------------|
40+
| NPA_USERNAME | The name of the NPA user created for your group: `${group_name}_npa` |
41+
| NPA_PASSWORD | The personal access token with API scope generated for the NPA user |
42+
43+
## Pipeline configuration
44+
45+
The pipeline configuration below will:
46+
1. Generate a unique version tag based on git describe
47+
2. Update the version for every build on the `master` branch.
48+
3. Tag the docker image built with the updated version as `latest` only for `tag` builds.
49+
50+
This pipeline omits steps for building the project and pushing the resulting Docker image to the registry.
51+
52+
```
53+
stages:
54+
- generate-env-vars
55+
- version
56+
- tag-latest
57+
58+
variables:
59+
IMAGE_NAME: $CI_REGISTRY/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME
60+
61+
generate-env-vars:
62+
stage: generate-env-vars
63+
script:
64+
- TAG=$(git describe --tags --always)
65+
- echo "export TAG=$TAG" > .variables
66+
- echo "export IMAGE=$IMAGE_NAME:$TAG" >> .variables
67+
- cat .variables
68+
artifacts:
69+
paths:
70+
- .variables
71+
72+
version:
73+
stage: version
74+
image: mrooding/gitlab-semantic-versioning:1.0.0
75+
script:
76+
- python3 /version-update/version-update.py
77+
only:
78+
- master
79+
80+
tag-latest:
81+
stage: tag-latest
82+
image: docker:18.06.1-ce
83+
before_script:
84+
- source .variables
85+
script:
86+
- docker pull $IMAGE
87+
- docker tag $IMAGE $IMAGE_NAME:latest
88+
- docker push $IMAGE_NAME:latest
89+
only:
90+
- tag
91+
```
92+
93+
## Merge request instructions
94+
95+
### Squash commits when merge request is accepted
96+
97+
The new version will be determined based on the commit message. GitLab will automatically format a merge request commit message if the 'Squash commits when merge request is accepted` checkbox is checked during merge request creation.
98+
99+
This workflow relies on that commit message format and will fail the pipeline if it cannot extract the merge request id from the commit message.
100+
101+
Unfortunately, GitLab [doesn't yet allow for setting the checkbox to checked by default](https://gitlab.com/gitlab-org/gitlab-ce/issues/27956). Until implemented, make sure to manually check the squash option upon creation.
102+
103+
### Add a label to indicate a minor or major update
104+
105+
As described above, if you want to perform a minor or major update, don't forget to add the appropriate label to your merge request.

requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
semver==2.8.1
2+
python-gitlab==1.6.0

version-update.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
#!/usr/bin/env python3
2+
import os
3+
import re
4+
import sys
5+
import semver
6+
import subprocess
7+
import gitlab
8+
9+
def git(*args):
10+
return subprocess.check_output(["git"] + list(args))
11+
12+
def verify_env_var_presence(name):
13+
if name not in os.environ:
14+
raise Exception(f"Expected the following environment variable to be set: {name}")
15+
16+
def extract_gitlab_url_from_project_url():
17+
project_url = os.environ['CI_PROJECT_URL']
18+
project_path = os.environ['CI_PROJECT_PATH']
19+
20+
return project_url.split(f"/{project_path}", 1)[0]
21+
22+
def extract_merge_request_id_from_commit():
23+
message = git("log", "-1", "--pretty=%B")
24+
matches = re.search(r'(\S*\/\S*!)(\d)', message.decode("utf-8"), re.M|re.I)
25+
26+
if matches == None:
27+
raise Exception(f"Unable to extract merge request from commit message: {message}")
28+
29+
return matches.group(2)
30+
31+
def retrieve_labels_from_merge_request(merge_request_id):
32+
project_id = os.environ['CI_PROJECT_ID']
33+
gitlab_private_token = os.environ['NPA_PASSWORD']
34+
35+
gl = gitlab.Gitlab(extract_gitlab_url_from_project_url(), private_token=gitlab_private_token)
36+
gl.auth()
37+
38+
project = gl.projects.get(project_id)
39+
merge_request = project.mergerequests.get(merge_request_id)
40+
41+
return merge_request.labels
42+
43+
def bump(latest):
44+
merge_request_id = extract_merge_request_id_from_commit()
45+
labels = retrieve_labels_from_merge_request(merge_request_id)
46+
47+
if "bump-minor" in labels:
48+
return semver.bump_minor(latest)
49+
elif "bump-major" in labels:
50+
return semver.bump_major(latest)
51+
else:
52+
return semver.bump_patch(latest)
53+
54+
def tag_repo(tag):
55+
repository_url = os.environ["CI_REPOSITORY_URL"]
56+
username = os.environ["NPA_USERNAME"]
57+
password = os.environ["NPA_PASSWORD"]
58+
59+
push_url = re.sub(r'([a-z]+://)[^@]*(@.*)', rf'\g<1>{username}:{password}\g<2>', repository_url)
60+
61+
git("remote", "set-url", "--push", "origin", push_url)
62+
git("tag", tag)
63+
git("push", "origin", tag)
64+
65+
def main():
66+
env_list = ["CI_REPOSITORY_URL", "CI_PROJECT_ID", "CI_PROJECT_URL", "CI_PROJECT_PATH", "NPA_USERNAME", "NPA_PASSWORD"]
67+
[verify_env_var_presence(e) for e in env_list]
68+
69+
try:
70+
latest = git("describe", "--tags").decode().strip()
71+
except subprocess.CalledProcessError:
72+
# Default to version 1.0.0 if no tags are available
73+
version = "1.0.0"
74+
else:
75+
# Skip already tagged commits
76+
if '-' not in latest:
77+
print(latest)
78+
return 0
79+
80+
version = bump(latest)
81+
82+
tag_repo(version)
83+
print(version)
84+
85+
return 0
86+
87+
88+
if __name__ == "__main__":
89+
sys.exit(main())

0 commit comments

Comments
 (0)