6.5 KiB
Executable File
title | date | draft | summary |
---|---|---|---|
Automated Changelogs on GitLab | 2023-05-15T22:38:55-03:00 | false | Changelog automation on GitLab CI |
Changelogs are good, mainly if you need to keep track of what was changed on a release. But they can be a pain to write, especially if you have a lot of commits, people working on the same project, lots of tasks, and so on. A good spot to put some automation.
There are a couple of ways we could make an automated changelog system, we will focus on making one that uses GitLab CI and the commit messages from the project. We will also take into consideration that releases are made through git tags.
For this, we will start with a few requirements:
- Plan on a commit message pattern, for example: "[TASK-200] Fixing something for a task on Jira";
- Have the release notes/changelogs on a specific part of pipeline (for example production release);
- The release notes generation will take part when creating a tag.
We will take advantage of these two commands:
git log --pretty=format:"%s" --no-merges <tag>..HEAD
- This will give us the commit messages from the last tag to the HEAD;git describe --abbrev=0 --tags
- This will give us the latest tag.
Creating a basic pipeline
Let's start by creating a basic pipeline that will run on the production release.
run:
script:
- echo "Running the pipeline"
.generateChangelog:
image: python:latest
stage: test
script:
- echo "Generating changelog..."
# Generate changelog here
artifacts:
name: changelog.txt
paths:
- changelog.txt
when: always
expire_in: 1 week
deploy:
stage: deploy
extends:
- .generateChangelog
rules:
- if: $CI_COMMIT_TAG
when: manual
environment: production
We will output the changelog into a file named changelog.txt
and then we will use the artifacts
keyword to save it.
Generating the changelog
Note that we set the image to be python:latest
on the .generateChangelog
job, this is because we will use a Python script to generate the changelog. Inside the code we will set two functions: one that will return the latest tag, and another that will get the commits between the latest tag and the HEAD.
To call commands on the OS we will use the subprocess
module, and to get the output from the command we will use the communicate()
function. In case of an error, we can further add some error handling (more on this later).
def get_last_tag():
pipe = sp.Popen('git describe --abbrev=0 --tags', shell=True, stdout=sp.PIPE, stderr=sp.PIPE)
prev_tag, err = pipe.communicate()
# If it returns 0, it means it was successful
if (pipe.returncode == 0):
return prev_tag.strip()
def get_commits():
prev_tag = get_last_tag().decode('utf-8')
print('Previous tag: ' + prev_tag)
pipe = sp.Popen('git rev-list ' + prev_tag + '..HEAD --format=%s', shell=True, stdout=sp.PIPE, stderr=sp.PIPE)
commits, err = pipe.communicate()
# Only dealing with 0 for now
if (pipe.returncode == 0):
commits = commits.strip().decode('utf-8').split('\n')
return commits
Now we should get a list of the commits that we want. Calling the function get_commits()
will return a string list with all the commits, but there could be some commits that we don't want to show on the changelog, for example: Merge branch 'master' into 'develop'
. This is where having a pattern will help.
def get_formatted_commits():
commits = get_commits()
formatted_commits = []
for commit in commits:
if commit.startswith('[TASK-') or commit.startswith('[BUG-'):
formatted_commits.append(commit)
return formatted_commits
This will give us only the important commit messages with the pattern that we want. We can further improve this by adding a regex, transforming formatted_commits
into a set
of Task Numbers, do some parsing, API calls, whatever we want. For now, we will keep simple and do the basic.
Writing the changelog
Now that we have the commits that we want, we can write them to a file. We will use the open
function to open the file and write the commits to it.
def write_changelog():
commits = get_formatted_commits()
with open('changelog.txt', 'w') as f:
for commit in commits:
f.write(commit + '\n')
Putting it all together on the pipeline yaml file
Now that we have the everything we want, we can put them all together on the pipeline yaml file.
run:
script:
- echo "Running the pipeline"
.generateChangelog:
image: python:latest
stage: test
script:
- echo "Generating changelog..."
- git tag -d $(git describe --abbrev=0 --tags) || true
- python changelog.py
artifacts:
name: changelog.txt
paths:
- changelog.txt
when: always
expire_in: 1 week
deploy:
stage: deploy
extends:
- .generateChangelog
rules:
- if: $CI_COMMIT_TAG
when: manual
environment: production
Note that we had to add git tag -d $(git describe --abbrev=0 --tags)
command there to delete the latest tag. This is because we are using the git describe
command to get the latest tag, and if we don't delete it, the changelog will be empty. The || true
is there to make sure that the pipeline doesn't fail if a tag doesn't exist.
Error handling
We can further improve this by adding some error handling. For example, if we don't have any tags, we can set a default hash (which would be the start of git history).
def get_last_tag():
pipe = sp.Popen('git describe --abbrev=0 --tags', shell=True, stdout=sp.PIPE, stderr=sp.PIPE)
prev_tag, err = pipe.communicate()
# If it's successful, we return the tag name
if (pipe.returncode == 0):
return prev_tag.strip()
else:
# If it's not successful, we return the first commit hash
pipe = sp.Popen('git rev-list --max-parents=0 HEAD', shell=True, stdout=sp.PIPE, stderr=sp.PIPE)
first_commit, err = pipe.communicate()
# If it's successful, we return the first commit hash
if (pipe.returncode == 0):
return first_commit.strip()
else:
# If it's not successful, we print the error and exit, there's something else wrong
print('Error: Could not get the last commit hash')
print(err.strip())
sys.exit(1)
Further error handling or improvements can be done, this is just a proof of concept. On another note, the code hasn't been tested as is, so there might be some errors.