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).

Continuous Integration

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 Delivery

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.).

Continuous Deployment

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, and Z 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, and Z 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

Branch: main

  • 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. The main 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 and Y are positive integers and z is the literal ‘z’
        • real example: release/2.3.z
      • Improper Naming Examples:
        • release/anything-i-want
        • release/2.3.0
          • violates naming conventions: should end with a ‘z’, not a digit

Branch: release/X.Y.z

  • 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 a release/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 and Y are positive integers and z is the literal ‘z’
        • real example: hotfix/2.3.z
      • Improper Naming Examples:
        • hotfix/anything-i-want
        • hotfix/2.3.0
          • violates naming conventions: should end with a ‘z’, not a digit

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, and Z 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, or hotfix/X.Y.z branch.
    • Lifespan: short
    • Naming Conventions:
      • Proper Naming Example: feature/ABC-987
      • Improper Naming Example: feature/anything-i-want
  • 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, or hotfix/X.Y.z branch.
    • Lifespan: short
    • Naming Conventions:
      • Proper Naming Example: bugfix/DEF-456
      • Improper Naming Example: bugfix/anything-i-want

Work Branches

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:

  1. the first commit strips the -SNAPSHOT suffix off of the version number (typically in the build file)
    • version X.Y.Z-SNAPSHOT becomes X.Y.Z
      • example: 2.3.0-SNAPSHOT becomes 2.3.0
    • a tag named X.Y.Z is associated to this first commit
      • example tag: 2.3.0
  2. 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 becomes X.Y.Z'-SNAPSHOT (where Z' = Z+1)
      • example: 2.3.0 becomes 2.3.1-SNAPSHOT

NOTE: Typically, these two commits are produced via an automated release process.

Release Process

An example release process would go as follows:

  1. all of the relevant Pull Requests (or Merge Requests) are merged into the main branch (the main release branch) where the version is 2.3.0-SNAPSHOT
  2. 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 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
  3. the version in the source code of main is updated to 2.4.0-SNAPSHOT manually by a developer in order for new development to continue
  4. the 2.3.0 release is tested and a bug is found
  5. the release branch release/2.3.z (maintenance branch) is created off of the tag 2.3.0 and the version in the source code of this branch is set manually by a developer to: 2.3.1-SNAPSHOT
  6. 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 the 2.3.0 release, with the testing done on an artifact versioned as 2.3.1-SNAPSHOT
  7. once testing is satisfactory on the 2.3.1-SNAPSHOT artifact, a release is created in the release/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 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
  8. subsequent patch/revision releases are performed in the release/2.3.z branch with tags 2.3.2, 2.3.3, etc. associated to commits living in this branch, and these bug-fixes are merged into main, if applicable, on a case-by-case basis, without altering the version number in the source code of main which should be at 2.4.0-SNAPSHOT

NOTE: All of the X.Y.0 tags should be associated to commits that live in the main branch, with subsequent patch/revision tags X.Y.Z' (where Z' is greater than 0) live either on main or on their respective release/X.Y.z (maintenance) branches.

Continuous Delivery Flow

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:

  1. the tag representing production deployment is: 2.3.1
  2. locate branch release/2.3.z or create branch release/2.3.z from the tag 2.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
  3. ensure that the software version inside release/2.3.z (or hotfix/2.3.z) branch is set to the next logical snapshot version
    • example: 2.3.2-SNAPSHOT
  4. perform the hot-fix work in the branch release/2.3.z (or hotfix/2.3.z) using the Pull Request (or Merge Request) process
  5. the hot-fix artifact will build and publish as version: 2.3.2-SNAPSHOT
  6. once the work is complete and adequately tested, a release is created and tagged in the release/2.3.z (or hotfix/2.3.z) branch using the release process with the tag 2.3.2
  7. 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 a hotfix/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.

Diagrams

DevSecOps Pipeline Architecture

Continuous Delivery Pipeline Architecture