A Git Workflow Model for Continuous Delivery
Continuous Delivery Flow (CDF), also known as Semantic Versioning Flow (SVF), is a trunk-based development model and an extension and evolution of GitHub Flow providing a practical and pragmatic Git workflow that supports teams and projects where software deployments are made regularly using a Continuous Delivery process.
This guide explains how and why Continuous Delivery Flow (or Semantic Versioning Flow) works for DevOps or DevSecOps pipelines. If you are looking for an alternative to Git Flow and something with a little more process defined than what GitHub Flow has to offer, especially around how software versioning and branching work together, then this might be what you are looking for.
CDF does not stop at just branching and versioning software. It is designed to be integrated into a full DevSecOps solution. This allows for full integration of security scanning and test automation of software, improving the efficiency, quality, and risk associated with a Software Development Life Cycle (SDLC).
Terminology
Continuous Integration
Continuous Integration (CI) is the process of frequently integrating work, from multiple developers, into an integration branch, which is typically a release (candidate) branch. Continuous Integration is typically the default starting point for any software build process (pipeline).
What comes next, after Continuous Integration, is an extension and evolution that eventually becomes Continuous Delivery, and with continued evolution results in Continuous Deployment.
Continuous Delivery
Continuous Delivery (CD) builds upon Continuous Integration (CI) by adding a deployment component to the process. This is the process of moving software, that has been built, tested, deployed, and verified in various lower (non-production) environments, into release candidates that may or may not end up in a production environment. A strong versioning strategy is required for Continuous Delivery, the most popular being Semantic Versioning. A release candidate is designated to be either a “good release” or a “bad release”, and only “good releases” are considered for deployment to production.
Continuous Deployment
Continuous Deployment is the process of promoting software straight into production, using a fully automated build process consisting of a test suite that provides confidence and proves that the build is “production-ready”. There are no release candidates in Continuous Deployment. As soon as a change is committed to the software's main integration branch, the build is either successful or it is not – if it is successful then it gets deployed immediately to production. A strong versioning strategy is not required here because the most recent source code in the main integration branch is what is in production, so any arbitrary identifier can be used as a “version” (example: commit hash, build number, etc.).
NOTE: Continuous Deployment is rarely achieved and should only be considered once a solid Continuous Delivery pipeline is in place.
Versioning
Specification
In a Continuous Delivery Flow, the most common versioning specification to use is: Semantic Versioning
Snapshots
A snapshot version (X.Y.Z-SNAPSHOT
) represents the goal you are trying to get to. For example, the snapshot version 2.3.1-SNAPSHOT
indicates that the goal is to get to the release version 2.3.1
. Consequently, the act of releasing 2.3.1-SNAPSHOT
means that you have “achieved your goal” and therefore the release process will produce the release artifact versioned as: 2.3.1
Snapshot artifacts are temporary, disposable, and mutable (overwritable) artifacts that are used to verify a piece of functionality (feature, bug-fix, etc.). Snapshot artifacts can never make it to a higher or production environment – they are only used in lower environments to verify code. Snapshot artifacts get published to a snapshot repository.
NOTE: Branches in Git represent SNAPSHOTS
The source code at the HEAD of every single branch in a Git repo should contain a snapshot version in the build file, in the format
X.Y.Z-SNAPSHOT
where:
X
,Y
, andZ
are positive integers- the versioning follows the specification: Semantic Versioning
Releases
In a Continuous Delivery Flow, a release version (X.Y.Z
) represents an artifact that is potentially ready to be deployed to production (it is a release candidate), it may not necessarily reach production for a variety of reasons.
Release artifacts are permanent, immutable artifacts that can be deployed to any environment. Release artifacts get published to a release repository. A release candidate can be a failure, and in such cases, that release version number cannot be re-used, and a new release with a new release version number must be created to fix whatever issues were found in the failed release. Release artifacts should live forever in their release repository unless explicitly deleted.
NOTE: Tags in Git represent RELEASES
In order to view the source code of a release (
X.Y.Z
) that is currently running in any environment, you must refer to the Git tag associated to the version of the code running in that environment. Tags, not branches, in Git are associated to releases because branches, regardless of their name, are associated to snapshots. See Release Process to better understand why this is.
Branching
Misconceptions
There are several misconceptions about the purpose of certain branches due to the misunderstanding of whether Continuous Delivery vs Continuous Deployment is implemented.
The notion that the main
branch (or any other release branch) represents the code in production is simply not true in a Continuous Delivery process – this is only true in a Continuous Deployment process where every (successful) code commit is pushed to production. In the Continuous Delivery Flow, the code deployed to production is a RELEASE and consequently is associated to a Git tag (not a Git branch, such as main
, or any other branch).
NOTE: Branches that are called
release/X.Y.z
still represent a SNAPSHOT. Remember: the HEAD of any given branch is a SNAPSHOT (the goal being: to get to a RELEASE, with that RELEASE being associated to a Git tag).
There are work branches and there are release candidate (integration, maintenance) branches. Work branches are temporary and release branches have various lifespans, with the main
branch living forever.
Integration Branches
These types of branches are where release candidates are created from. These branches can act as release maintenance branches or for developing various releases in parallel. The version of the software must be unique within each release candidate branch (no two release candidate branches can share the same version number).
The version specified in the HEAD of this type of branch should always be a SNAPSHOT version in the build file, in the format X.Y.Z-SNAPSHOT
where:
X
,Y
, andZ
are positive integers- the versioning follows the specification: Semantic Versioning
There are 3 types of release candidate (integration, maintenance) branches:
- Branch:
main
- This is the main release branch that contains the latest software code.
- Lifespan: forever
- Branches:
release/X.Y.z
- Long-living
release/X.Y.z
branches are only required if there are multiple versions of the software that will be deployed to production, or if there are overlapping releases, or if a maintenance branch is required. Themain
branch should suffice as the main release branch in most simple use-cases. - Lifespan: long
- Naming Conventions:
- Proper Naming Example:
release/X.Y.z
- where
X
andY
are positive integers andz
is the literal ‘z’ - real example:
release/2.3.z
- where
- Improper Naming Examples:
release/anything-i-want
release/2.3.0
- violates naming conventions: should end with a ‘z’, not a digit
- Proper Naming Example:
- Long-living
- Branches:
hotfix/X.Y.z
- The
hotfix/X.Y.z
branches should only be used in very rare occasions as most hot-fix scenarios can be accomplished in arelease/X.Y.z
branch (also considered a maintenance branch). Hot-fix branches are only necessary when there are multiple releases and all available release branches are occupied with on-going work and cannot perform an adequate or urgent hot-fix. - Lifespan: short
- Naming Conventions:
- Proper Naming Example:
hotfix/X.Y.z
- where
X
andY
are positive integers andz
is the literal ‘z’ - real example:
hotfix/2.3.z
- where
- Improper Naming Examples:
hotfix/anything-i-want
hotfix/2.3.0
- violates naming conventions: should end with a ‘z’, not a digit
- Proper Naming Example:
- The
NOTE: Example list of branches in a project:
main
(HEAD = 2.5.0-SNAPSHOT)release/2.3.z
(HEAD = 2.3.2-SNAPSHOT)release/2.1.z
(HEAD = 2.1.2-SNAPSHOT)release/1.5.z
(HEAD = 1.5.3-SNAPSHOT)release/1.1.z
(HEAD = 1.1.9-SNAPSHOT)release/1.0.z
(HEAD = 1.0.7-SNAPSHOT)- …
NOTE: When following these conventions:
- the HEAD of every branch is a SNAPSHOT version number
- no version number at the HEAD of any release candidate branch can conflict with the HEAD of any other release candidate branch
Work Branches
These types of branches should be associated to a Pull Request (PR) or Merge Request (MR) at some point in time, and, consequently, these branches are considered to be temporary. These branches are typically merged into an Integration Branch.
The version specified in the HEAD of this branch should always be a SNAPSHOT version in the build file, in the format X.Y.Z-SNAPSHOT
where:
X
,Y
, andZ
are positive integers- the versioning follows the specification: Semantic Versioning
There are 2 types of work branches:
- Branches:
feature/*
- This type of branch is where work is done to complete a feature request from an active issue (Example: a feature request where the issue ID is: ABC-987). This branch must be attached to a Pull Request or Merge Request and merged to either
main
,release/X.Y.z
, orhotfix/X.Y.z
branch. - Lifespan: short
- Naming Conventions:
- Proper Naming Example:
feature/ABC-987
- Improper Naming Example:
feature/anything-i-want
- Proper Naming Example:
- This type of branch is where work is done to complete a feature request from an active issue (Example: a feature request where the issue ID is: ABC-987). This branch must be attached to a Pull Request or Merge Request and merged to either
- Branches:
bugfix/*
- This type of branch is where work is done to complete a bug-fix request from an active issue (Example: a bug-fix request where the issue ID is: DEF-456). This branch must be attached to a Pull Request or Merge Request and merged to either
main
,release/X.Y.z
, orhotfix/X.Y.z
branch. - Lifespan: short
- Naming Conventions:
- Proper Naming Example:
bugfix/DEF-456
- Improper Naming Example:
bugfix/anything-i-want
- Proper Naming Example:
- This type of branch is where work is done to complete a bug-fix request from an active issue (Example: a bug-fix request where the issue ID is: DEF-456). This branch must be attached to a Pull Request or Merge Request and merged to either
Release Process
The main release branch should always be the main
branch. Consequently, main release Git tags should be created on the main
branch history (commits).
A release/X.Y.z
branch should not be used as a main release branch. These types of branches are, primarily, created as maintenance branches in order to fix bugs in a release (related to a release that was tagged on the main
branch), or, secondarily, to work on a parallel (or overlapping) release alongside the main
branch. If multiple release branches are required then main
should be used for the latest version of the software, and other versions of the software should go in release branches that follow the naming convention release/X.Y.z
where X
and Y
are positive integers and z
is the literal ‘z’. Example: release/2.3.z
When a release is created in a given branch, two commits are necessary in order to appropriately create a release artifact:
- the first commit strips the
-SNAPSHOT
suffix off of the version number (typically in the build file)- version
X.Y.Z-SNAPSHOT
becomesX.Y.Z
- example:
2.3.0-SNAPSHOT
becomes2.3.0
- example:
- a tag named
X.Y.Z
is associated to this first commit- example tag:
2.3.0
- example tag:
- version
- the second commit auto-increments the last digit
Z
(the patch/revision number) in the version number, and re-appends the-SNAPSHOT
suffix- version
X.Y.Z
becomesX.Y.Z'-SNAPSHOT
(whereZ'
=Z+1
)- example:
2.3.0
becomes2.3.1-SNAPSHOT
- example:
- version
NOTE: Typically, these two commits are produced via an automated release process.
An example release process would go as follows:
- all of the relevant Pull Requests (or Merge Requests) are merged into the
main
branch (the main release branch) where the version is2.3.0-SNAPSHOT
- a release is created in the
main
branch using the release process- the first commit strips the
-SNAPSHOT
suffix off of the version number, in this case the version results in:2.3.0
- the tag
2.3.0
is created and points to this first commit (where the source code has the version suffix-SNAPSHOT
removed)
- the tag
- the second commit auto-increments the last digit
Z
(the patch/revision number) in the version number, in this case the new version results in:2.3.1-SNAPSHOT
- the first commit strips the
- the version in the source code of
main
is updated to2.4.0-SNAPSHOT
manually by a developer in order for new development to continue - the
2.3.0
release is tested and a bug is found - the release branch
release/2.3.z
(maintenance branch) is created off of the tag2.3.0
and the version in the source code of this branch is set manually by a developer to:2.3.1-SNAPSHOT
- bug-fix work is done in the
release/2.3.z
branch using the Pull Request (or Merge Request) process to fix the bug found in the2.3.0
release, with the testing done on an artifact versioned as2.3.1-SNAPSHOT
- once testing is satisfactory on the
2.3.1-SNAPSHOT
artifact, a release is created in therelease/2.3.z
branch using the release process- the first commit strips the
-SNAPSHOT
suffix off of the version number, in this case the version results in:2.3.1
- the tag
2.3.1
is created and points to this first commit (where the source code has the version suffix-SNAPSHOT
removed)
- the tag
- the second commit auto-increments the last digit
Z
(the patch/revision number) in the version number, in this case the new version results in:2.3.2-SNAPSHOT
- the first commit strips the
- subsequent patch/revision releases are performed in the
release/2.3.z
branch with tags2.3.2
,2.3.3
, etc. associated to commits living in this branch, and these bug-fixes are merged intomain
, if applicable, on a case-by-case basis, without altering the version number in the source code ofmain
which should be at2.4.0-SNAPSHOT
NOTE: All of the
X.Y.0
tags should be associated to commits that live in themain
branch, with subsequent patch/revision tagsX.Y.Z'
(whereZ'
is greater than 0) live either onmain
or on their respectiverelease/X.Y.z
(maintenance) branches.
NOTE: If this release process does not work for you then you are not following Semantic Versioning correctly.
Hot-fix Scenario
In the unlikely event that an active deployed production release requires an urgent hot-fix then the Git tag for that active production deployment should be found and a Git branch should be created off of that tag following the naming convention release/X.Y.z
where X
and Y
are positive integers and z
is the literal ‘z’ (assuming that such a branch does not already exist as there may already be an active and suitable release branch available to perform the hot-fix to do a patch/revision release).
The following example demonstrates the steps for hot-fix-ing a production deployment with version 2.3.1
:
- the tag representing production deployment is:
2.3.1
- locate branch
release/2.3.z
or create branchrelease/2.3.z
from the tag2.3.1
- if it is not possible to use branch
release/2.3.z
to perform the hot-fix for whatever reason then create branch:hotfix/2.3.z
- if it is not possible to use branch
- ensure that the software version inside
release/2.3.z
(orhotfix/2.3.z
) branch is set to the next logical snapshot version- example:
2.3.2-SNAPSHOT
- example:
- perform the hot-fix work in the branch
release/2.3.z
(orhotfix/2.3.z
) using the Pull Request (or Merge Request) process - the hot-fix artifact will build and publish as version:
2.3.2-SNAPSHOT
- once the work is complete and adequately tested, a release is created and tagged in the
release/2.3.z
(orhotfix/2.3.z
) branch using the release process with the tag2.3.2
- this bug-fix is then merged into
main
branch (if applicable)
NOTE: Release candidate (maintenance) branches (
release/X.Y.z
) should be sufficient to perform a hot-fix in the vast majority of hot-fix cases. Only create ahotfix/X.Y.z
branch if there is an edge-case that conflicts with using a release candidate (maintenance) branch:release/X.Y.z
NOTE: If this hot-fix process does not work for you then you are not following Semantic Versioning correctly.