During my internship at Invesco, I had the opportunity to work across a wide range of technologies and projects, from building trade communication dashboards to designing Flyway migrations for Snowflake databases, and even optimizing project builds to achieve a 300% speed increase. Among these projects was designing and implementing an automated release branch system within a core CI/CD pipeline. This pipeline supported a suite of AWS Lambda functions responsible for parsing and processing securities data, a critical component of the firmâs data infrastructure.
For this article, Iâll be focusing specifically on the automated release branching system rather than the entirety of my summer experience (to keep things conciseâand to make sure I donât accidentally disclose too much).
Before diving into the details of the project, itâs important to cover two key areas of context: (1) the CI/CD tech stack in use, and (2) the branching strategy that guided development. Understanding these elements will make the design choices and overall workflow much clearer.
First, rather than GitHub, Invesco leverages Bitbucket as its Git repository management solution. Bitbucket provides not only version control but also an integrated cloud environment and configurable pipeline steps, all managed through a bitbucket.yaml file. Complementing this setup were standard Git operations and a Python package called Commitizen, which Iâll revisit later when explaining how the automation was built.
Second, the branching strategy followed a fairly structured release-oriented workflow:
One more crucial detail: the entire system adhered to semantic versioning. Every release carried a MAJOR.MINOR.PATCH version, ensuring clear communication about backward compatibility, feature additions, and patches.
At first glance, the release branching process might seem perfectly adequateâso why bother automating it? The answer comes down to two key reasons: convenience and reliability.
On the convenience side, automation removes the need for developers to manually update release versions. This saves time and reduces repetitive, low-value work, letting engineers focus on building features instead of managing branches.
More importantly, manual processes introduce risk. Developers could forget to create a pull request, merge a release branch, or cut a new release version. These gaps can lead to version conflicts at critical points in the pipeline. Since production deployments are directly tied to release branches, any mistakes here ripple outward, potentially impacting real usersâportfolio managers. In fact, this wasnât just a hypothetical risk; there had been past incidents where releases had to be rolled back due to version mismatches and unexpected code conflicts.
Thus by automating the release branching process, we significantly reduce human error while ensuring consistency and stability in production deployments.
The drafted solution for automation had two main components. First, upon a successful deployment to the main production environment, an email notification would be sent to the responsible developer reminding them to merge the release branch into the master branch. If the production deployment failed, Bitbucketâs built-in alerting system would already handle notifying the team, so the error feedback loop was covered.
Initially, I assumed this would be straightforward since Invesco offered an internal âpipeâ (a set of preconfigured steps) for sending emails. However, to no oneâs surprise, the pipe ran into runtime errors and after reaching out to the cloud team, I was told the issue would require a support ticket that could take an indefinite amount of time to resolve.
Rather than let the project stall, I explored an alternative: sending emails directly via a Python script using the SMTP protocol. Since security settings were already configured to restrict execution to specific environments, this approach fit neatly into the existing infrastructure. Thanks to prior experience working with Python and email automation, I was able to build and test the script quickly and after a few iterations, the email notification system was up and running smoothly.
After a pull request from a release branch was merged into master, it would trigger an automated pipeline for the master branch. This is where I focused the remainder of my work. The pipeline needed to accomplish four key tasks:
Steps 2â4 were relatively straightforwardâjust a series of Git commands. The real challenge lay in Step 1: automatically determining the correct release version.
The engineer I worked with initially suggested Commitizen, a tool designed to infer semantic version bumps by parsing Git commit messages. For example, a commit message like âbreak: introduced breaking changesâ would trigger a major release bump. Commitizen was also configurable, so I could change things like which keywords associated with which version bump which seemed perfect.
However, I quickly ran into friction. Commitizenâs default parser required a keyword + colon format (e.g., feat: add dashboard), which meant every developer would need to adapt their commit messages. On my team, the convention was simpler: unless a commit explicitly introduced a breaking change or hotfix, it should bump the minor version. Forcing developers to type âfeat:â just to get a minor bump felt like unnecessary friction, and worse, commits without keywords werenât recognized at all.
To reduce friction, I experimented with Commitizenâs configurability and discovered I could insert regex-based rules into the keyword parser. I configured it so that anything not explicitly marked as break, fix, or hotfix would automatically default to a minor bump. However, Commitizen still required a colon delimiter for parsing, which was a painful quirk.
Frustrated, I briefly explored building a custom Bash-based tool to parse commit messages and determine version bumps. In theory, this was simple: read the messages, categorize them, and increment the version. In practice, it was messy. Automated merge commits, different merging strategies, and inconsistent message formats all created too many edge cases. I quickly realized maintaining such a tool long-term would be more of a liability than a solution, so I scrapped the idea.
Eventually, after scouring Commitizenâs documentation and GitHub repo, I found the breakthrough: I can parse the full-message by including the entire message as the bump pattern parameter. With this, I could define my own prefixes and rules to match exactly what the team needed, without disrupting developer workflows. Once Commitizen was properly configured, the rest of the pipeline steps were straightforward. Example configuration below (not 100% accurate):
[tool.commitizen]
name = "cz_customize"
[tool.commitizen.customize]
bump_pattern = "^(.*)"
bump_map = {"^(break)" = "MAJOR", "" = "MINOR", "^(fix|hotfix)" = "PATCH"}
Of course, there were still some quirks to iron out. For example, Git doesnât allow tags and branches to share the same name. And to prevent infinite loops, I had to ensure that any automated commits included [skip ci] in the commit message to stop the pipeline from retriggering itself.
This project was one of the more fun tasks that I accomplished during my internship. Beyond the technical challenge, it gave me a deeper understanding of codebase management, branching strategies, and CI/CD automation at scale. Seeing the solution go liveâand then adopted across other Lambda-related repositories within the teamâwas especially gratifying. It was a reminder that even relatively small workflow improvements can have a meaningful impact on developer efficiency and production stability.
Thanks for reading. Cya đ.