DEV Community

Cover image for Spring cleanup: Outdated Git tags
Nils Diekmann
Nils Diekmann

Posted on • Originally published at Medium

Spring cleanup: Outdated Git tags

Imagine your repository is flooded with auto-generated Git tags. Most of them are from feature branches you no longer remember anymore. Just as you strive to keep your code clean, you should also regularly clean up these Git tags.

Spring cleanup


Motivation

Recently I started programming with Go. I have written some modules and in order to use them in other projects, I need to publish them. Go uses Git tags to mark a specific version. The tags should use semantic versioning. I am already familiar with semantic versioning. In C#, I used this simple approach to give my services and NuGet packages a descriptive version.

Git version tag

I am a lazy person and I like to automate all my work. So I invested blood and tears in automatically tagging my code with corresponding semantic versions. Having even managed to handle multiple modules in one repository, I am now facing the next challenge.

I use feature branches to develop and pre-release my code. The pipeline generates new Git tags with each check-in, which is useful during development. But how do I get rid of them, after I merge the feature branch? I don't want to waste my life manually deleting things.

The full code for the article is available on GitHub. The repository also has an open feature branch for demonstration purposes.

GitHub logo KinNeko-De / sample-git-tag

Sample application how to cleanup outdated Git tags.

sample-git-tag

Sample application how to cleanup outdated Git tags.

See my article how to create this by yourself.




Step 0: Producing semantic versioning tags

Semantic versioning is a popular versioning scheme that uses a format like "major.minor.patch" to indicate the version of the code base. My GitHub workflow has an environment variable 'MAJOR_MINOR_PATCH' which you must manually update according to the rules of semantic versioning when you develop the code.

Semantic version git tag

For the main branch, the semantic version is determined by the environment variable. For feature branches, a pre-release version is defined by a suffix containing the name of the branch and the build run number: v0.1.0-feature-branch.1. The workflow then pushes the semantic version with actions/github-script as a Git tag. Adding the build run number makes the Git tag unique without manual intervention.

Step 1: Delete outdated tags with a script

When I need to automate my task, I start with writing a script that I can run and debug locally. The first step is to have a good design. I want to delete the git tag from the remote repository. Outdated git tags are defined by git tags, that contain a semantic prerelease version and where the corresponding feature branch was already deleted.

  1. I need to update my local version to the remote version.
  2. I have to fetch all git tags.
  3. I have to fetch all branches.
  4. Determine for each git tag if it is outdated.
  5. If so, delete the git tag on the remote repository.

git fetch --tags --prune --prune-tags will fetch branches and tags from the remote version and delete any local branches and tags that no longer exist on the remote.

git branch -r retrieves all remote branches. Then the names are normalized by removing slashes and whitespaces with sed 's/^*//;s/ *$//. Finally, I filter out my main branch with egrep -v "^main".

git tag - list 'v[0–9]*\.[0–9]*\.[0–9]*-* gives me a list of all git tags, where the name starts with a v followed by the major, minor, and patch version numbers. It also must be followed by a hyphen - and any character that indicates a pre-release version.

I have the convention that all my feature branches start with feature followed by the name of the branch. But the git tag only contains the name of the branch. An example feature/branchname for the feature branch and the corresponding git tag is then v1.2.0-branchname.

[[ $tag =~ ^v[0-9]*\.[0-9]*\.[0-9]*-(.*)\.([0-9]*)$ ]]
featurebranchname=${BASH_REMATCH[1]}
Enter fullscreen mode Exit fullscreen mode

For each of the git tags I cut the feature branch name out (see code block above) and check if a feature branch with a name according to my conversion exists (see code block below).

if [[$existingfeaturebranches =~ "feature/$featurebranchname" ]]
Enter fullscreen mode Exit fullscreen mode

If not so, then the git tag is first deleted on the origin git push origin --delete $tag and then locally git tag -d $tag.

Step 2: Integrate the script into a CI pipeline

Automating tasks with scripts is a valuable first step, but manual execution is time-consuming. Integrating the script into my CI pipeline, triggered on every code push, would be a more convenient approach.

To create this step for my workflow, I ask GitHub Copilot to translate the bash script into a step for my GitHub workflow. Like me, GitHub Copilot prefers to use actions/github-script. Unexpectedly, GitHub Copilot generated JavaScript code instead of Bash. It seems that JavaScript is the preferred language for GitHub actions.

The logic of how the code fetches the branches has changed. GitHub Copilot missed that I was filtering out the main branch. My Git tags for the main branch have the format v1.2.0 and do not include the name of the main branch itself. Other than the fact that the main branch is now logged as an existing feature branch, the change does not affect the result.

const existingFeatureBranches = (await github.rest.repos.listBranches({
  owner: context.repo.owner,
  repo: context.repo.repo,
})).data.map(branch => branch.name);
Enter fullscreen mode Exit fullscreen mode

Another change is that Git tags are no longer filtered to feature branch tags only. Aside from the longer runtime, this does not change the logic, since the other tags do not match the feature branch regular expression anyway. I keep the code as it is and consider this a limitation of the GitHub Rest API.

const tags = await github.rest.git.listMatchingRefs({
  owner: context.repo.owner,
  repo: context.repo.repo,
  ref: 'tags/v',
});
Enter fullscreen mode Exit fullscreen mode

The workflow with the script is triggered every time I push code. I do this a lot to break problems into smaller steps. Since deleting git tags takes time, I do not want to run the script on every push. My repository is configured to automatically delete the feature branch when a pull request completes. After that, the workflow is triggered for the main branch. If I run the action on every run of the workflow for the main branch, it will fit my needs perfectly.

- name: Delete Git Tags
  uses: actions/github-script@v7
  if: github.ref == 'refs/heads/main'
Enter fullscreen mode Exit fullscreen mode

To achieve this goal, I introduce a conditional check within the pipeline using the if keyword. The branch that triggered the current run of the workflow is defined by github.ref. In my case, the name of my main branch is refs/heads/main. The step will be now only executed if the two variables have the same value.

Finally, you can also take a look at my first GitHub action that replicates the functionality of the step inside this sample pipeline. The GitHub action is implemented using TypeScript. It's important to note that this GitHub action is specifically tailored to my conventions and lacks flexibility. Under these miserable conditions, I recommend understanding the underlying concepts before directly using my GitHub action in your projects. Instead, feel free to copy and modify the code to suit your needs.

GitHub logo KinNeko-De / cleanup-outdated-tag-action

GitHub action to cleanup outdated Git tags.

Create a GitHub Action Using TypeScript

GitHub Super-Linter CI Check dist/ CodeQL Coverage

Use this template to bootstrap the creation of a TypeScript action. 🚀

This template includes compilation support, tests, a validation workflow publishing, and versioning guidance.

If you are new, there's also a simpler introduction in the Hello world JavaScript action repository.

Create Your Own Action

To create your own action, you can use this repository as a template! Just follow the below instructions:

  1. Click the Use this template button at the top of the repository
  2. Select Create a new repository
  3. Select an owner and name for your new repository
  4. Click Create repository
  5. Clone your new repository

Important

Make sure to remove or update the CODEOWNERS file! For details on how to use this file, see About code owners.

Initial Setup

After you've cloned the repository to your local machine or codespace, you'll need to perform some initial setup steps before you can…

I'm excited to share my experience and encourage others to explore the potential of GitHub Actions for my automation needs. Thank you for reading my story until the end. ❤

Top comments (0)