Skip to content

Commit 8344022

Browse files
authored
[post] Versioning (#25)
* Initial commit of automated versioning * Start writing on how to approach thinking about the software version * implementation portion of auto versioning * Add conclusion and check spelling * Prepping for publishing
1 parent 7f1b39a commit 8344022

File tree

1 file changed

+226
-0
lines changed

1 file changed

+226
-0
lines changed
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
!! title: Scalable Development Processes - Automatic Versioning
2+
!! slug: scalable-dev-versioning
3+
!! published: 2023-08-14
4+
!! description: The fourth article in the series of how to implement a simple but scalable solution to delivering more value faster to the end user
5+
6+
---
7+
In the last few articles, we have been looking at how to implement scalable dev processes to support growing
8+
organization sizes and deliver more value faster and how to sustain that velocity long term. So far, we have looked at
9+
Continuous Integration and what that needs to look like to support the next processes. We walked through an example of
10+
what CI would look like for a simple application. We then looked at Continuous Delivery and walked through an example
11+
of CD with the same simple application. Before getting to Continuous Deployment, we are going to pause and talk about
12+
software versioning and how to approach it in a Continuous Deployment world.
13+
14+
15+
## What is versioning?
16+
17+
This seems like a simple question with a very obvious answer. However, most of the projects that I have worked on have
18+
not approached versioning from the most effective direction. There are a lot of different versioning schemes out there:
19+
SemVer vs CalVer vs iterating build numbers. The most effective approach to versioning is aligning the implementation to
20+
the primary message that needs to be communicated to all of the project stakeholders.
21+
22+
Let's take a small step back and look at a few perspectives around project versioning. What does software versioning do?
23+
One might suggest _"to help track when changes are made to the software"_. Some software uses date based versioning to
24+
show how up-to-date software is and help build support policies (I'm thinking of Ubuntu). Or maybe _"to help track when
25+
specific features were changed in the software"_. Hypothetically, if we are updating and releasing the software with
26+
every change to trunk, our software has the opportunity to be changing very rapidly and with partial features being
27+
deployed to Production behind feature flags. What about _"showing compatibility between other software and hardware"_?
28+
It's important to know what is required to run the software as it changes.
29+
30+
Each one of these answers can be accurate in a certain scenario. However, I would like to suggest that they _all_ stem
31+
from a single purpose: to communicate something to someone. When it is all boiled down, versioning is a tool to
32+
communicate with all project stakeholders about changes to the software. To have effective versioning, one must consider
33+
who all of the audiences are, what they care about, if they have any expectations, and what information to convey.
34+
35+
Who are the stakeholders for the versioning of the project? There are internal stakeholders in the QA and Engineering
36+
teams that might care about which version their testing and what is different between this version and that version. The
37+
end user might care about which version of the software they are running to see if their hardware is compatible. And if
38+
the project is an SDK or library for other engineers to use, they will definitely care about any changes to the
39+
compatibility of the project within their own. But in other cases, there are no version stakeholders. This site, while
40+
under version control, does not have a specific released version.
41+
42+
Once the _who_ has been identified, the _what_ comes next. What is primary message that needs to be communicated to the
43+
stakeholders? Keep the primary goal the focus of the approach to versioning. If by happenstance there is a secondary
44+
communication benefit, that's great. But don't try to over engineer the message of the version itself. Do not be afraid
45+
to have a secondary communication device used alongside the version to help the interested stakeholders understand any
46+
additional communication goals that you might have. For example, a compatibility matrix could be useful to communicate
47+
the compatibility between a version of the software and the type of hardware that it supports.
48+
49+
50+
## How to approach in-software versioning in CI/CD
51+
52+
A lot of frameworks approach in-software versioning as a build-time configuration. This means that for the version to be
53+
shown via the software interface (a `--version` on the cli or a footer with the version in it for a web app), it has to
54+
be set before the artifact is built. Most approaches that I have seen sets and tracks the current version in SCM
55+
alongside the code. This is a quick and easy way to track it with small teams. However, it does not scale to multiple
56+
engineers pushing changes to trunk simultaneously. If two engineers push a minor version bump and a patch at the same
57+
time, which one goes into the trunk first and which engineer has to fix the merge conflict in the file tracking the
58+
version?
59+
60+
Automation seems to be the solution. The engineers can mark their change on how the version should change when their
61+
change is merged in and automation can calculate what the end version should be without the engineer needing to figure
62+
it out. This can even be implemented in a [merge queue](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/configuring-pull-request-merges/managing-a-merge-queue)
63+
implementation. But one question remains unsolved: for the frameworks that require a version in the codebase, how do we
64+
update it without risking a conflict? The version automation could be built to commit and merge a version bump straight
65+
to trunk, but in a high velocity repo that might conflict with other PRs coming in and updating the version.
66+
67+
It wasn't until I read the article on the [GLEW](https://sam.gleske.net/blog/engineering/2019/11/12/git-low-effort-workflow.html)
68+
approach to implementing trunk based development that I realized that the software version should not be tracked in the
69+
trunk. Instead, git tags are used for the version source of truth. The third and fifth factors in the
70+
[12 Factor App](https://12factor.net/build-release-run) helps conceptualize the difference between a build artifact and
71+
a release asset: application configuration should not be stored in the codebase alongside the app, but should be stored
72+
elsewhere and packaged later with the build artifact to create the final release asset. In the SaaS world, this is a
73+
trivial problem to solve since backend applications can rely on the environment variables for configuration and the web
74+
app can rely on their backend application to provide the correct configuration (a future article will show how to
75+
implement such a strategy).
76+
77+
There is a drawback to this approach. The version will never be tracked in trunk, so it will never be in the git
78+
history. For most applications, the git tags should be sufficient to track history, but there may be use cases where it
79+
is required. Those use cases would require some custom work to get to a scalable development process or to deprecate
80+
that use case.
81+
82+
83+
## Implementing Auto Versioning
84+
85+
There is a great tool called [GitVersion](https://gitversion.net/docs/) that takes an approach to adding to calculating
86+
the version from nothing but git history and special commit messages. They provide some robust configuration and tooling
87+
around implementing their approach. However, I think there is a process improvement that could be done. There are a few
88+
things that I do not trust myself to do as an engineer: remember to add the special commit message, mistakenly adding
89+
multiple on the same branch, and spelling the keywords correctly. There are also the questions about how to bubble this
90+
up to the PR description and how to handle if the version bump calculation is set incorrectly by the change. These are
91+
not impossible things to solve and all of them can be solved with automation. However, remembering the golden rule of
92+
automation, the process seems to be getting more complex. While something like GitVersion definitely works, I would
93+
suggest using PR labels. They provide solutions to the friction points listed and also provide a bit more of a intuitive
94+
developer experience.
95+
96+
Here's an example of such an implementation for GitHub specifically that is being used in the example project:
97+
98+
```yaml
99+
# .github/workflows/_version.yml
100+
101+
---
102+
name: _version
103+
run-name: Calculate Version
104+
105+
on:
106+
workflow_call:
107+
inputs:
108+
is-release:
109+
type: boolean
110+
required: true
111+
pull-request-number:
112+
type: string
113+
required: true
114+
outputs:
115+
version:
116+
description: "version to be built"
117+
value: ${{ jobs.version.outputs.version }}
118+
119+
jobs:
120+
version:
121+
name: Calculate Version
122+
runs-on: ubuntu-22.04
123+
outputs:
124+
version: ${{ steps.calculate.outputs.version }}
125+
steps:
126+
- name: Checkout Repo
127+
uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
128+
with:
129+
fetch-depth: 0
130+
131+
- name: Get version bump type
132+
if: ${{ inputs.is-release }}
133+
id: bump-type
134+
env:
135+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
136+
run: |
137+
version_tag=$(
138+
curl -s -L \
139+
-H "Accept: application/vnd.github+json" \
140+
-H "Authorization: Bearer $GH_TOKEN" \
141+
-H "X-GitHub-Api-Version: 2022-11-28" \
142+
https://api.github.com/repos/${{ github.repository }}/issues/${{ inputs.pull-request-number }}/labels | \
143+
jq -r ".[].name" | grep "version"
144+
)
145+
146+
# Single Version label Enforcement (should go in CI...)
147+
if [[ $(echo $version_tag | wc -w) -gt 1 ]]; then
148+
echo "[!] multiple version labels found!"
149+
exit 1
150+
fi
151+
152+
version_type=$(echo $version_tag | cut -d ":" -f 2)
153+
echo "Version Bump Type: $version_type"
154+
echo "type=$version_type" >> $GITHUB_OUTPUT
155+
156+
- name: Calculate next version
157+
id: calculate
158+
env:
159+
VERSION_TYPE: ${{ steps.bump-type.outputs.type }}
160+
run: |
161+
echo -e "\nCalculating next version..."
162+
163+
latest_tag_version=$(git tag --sort=committerdate --list | tail -1)
164+
latest_version=${latest_tag_version:1} # remove 'v' from tag version
165+
166+
if [[ "${{ inputs.is-release }}" == "true" ]]; then
167+
latest_major_version=$(echo $latest_version | cut -d "." -f 1)
168+
latest_minor_version=$(echo $latest_version | cut -d "." -f 2)
169+
latest_patch_version=$(echo $latest_version | cut -d "." -f 3)
170+
171+
echo " latest_version: $latest_version"
172+
echo " latest_major_version: $latest_major_version"
173+
echo " latest_minor_version: $latest_minor_version"
174+
echo " latest_patch_version: $latest_patch_version"
175+
176+
if [[ "$VERSION_TYPE" == "major" ]]; then
177+
next_version="$(($latest_major_version + 1)).${latest_minor_version}.${latest_patch_version}"
178+
elif [[ "$VERSION_TYPE" == "minor" ]]; then
179+
next_version="${latest_major_version}.$(($latest_minor_version + 1)).${latest_patch_version}"
180+
elif [[ "$VERSION_TYPE" == "patch" ]]; then
181+
next_version="${latest_major_version}.${latest_minor_version}.$(($latest_patch_version + 1))"
182+
else
183+
next_version="${latest_major_version}.${latest_minor_version}.${latest_patch_version}"
184+
fi
185+
186+
echo "Next Version: $next_version"
187+
echo "version=$next_version" >> $GITHUB_OUTPUT
188+
else
189+
echo "version=$latest_version+${{ inputs.pull-request-number }}" >> $GITHUB_OUTPUT
190+
fi
191+
```
192+
193+
In the GitHub repo, I have set up four Issue labels: `version:major`, `version:minor`, `version:patch`, and
194+
`version:skip`. The first step of the job is grabbing the label from the PR that triggered this workflow run and setting
195+
is as an output to use later.
196+
197+
The next step of the job is getting the latest git tag and then calculating the next tag to create from the output of
198+
the first job. For example, if the latest git tag is `v1.24.3` and the PR label is `version:minor`, the resulting
199+
calculated version will be `v1.25.0`.
200+
201+
202+
```yaml
203+
...
204+
205+
- name: Save version
206+
run: |
207+
cat version.json | jq '. + {"version": "${{ inputs.version }}"}' > tmpVersion.json
208+
mv tmpVersion.json version.json
209+
210+
cat version.json
211+
212+
...
213+
```
214+
215+
Once the version is calculated, it is past back to the `_build.yml` (via the `CI-feature-branch.yml` or `CI-main.yml`
216+
orchestrating workflows) and is saved and built into the artifact. The version change is not committed or pushed back to
217+
the codebase in either a detached state or in the trunk. The build artifact that was tested does not change, but the
218+
version configuration does. Combined, the release asset is ready to be published or deployed.
219+
220+
---
221+
222+
Software versioning is a tool used to communicate with project stakeholders. Determining all audiences and the message
223+
being communicated is important for effective communication. Once the versioning scheme has been designed, treating
224+
versioning as a configuration, even with frameworks that require a compile time version, allows for automating the
225+
versioning scheme. And this in turn enables the journey towards Continuous Deployment.
226+

0 commit comments

Comments
 (0)