utils.release

release.py

Common code useful for release pipelines.

Example Usage: from utils import release from utils.release import get_new_build_release as _version

  1#!/usr/bin/env python3
  2"""
  3release.py
  4
  5Common code useful for release pipelines.
  6
  7Example Usage:
  8    from utils import release
  9    from utils.release import get_new_build_release as _version
 10"""
 11
 12import datetime
 13# from subprocess_tee import run as _run
 14import os
 15import sys
 16from subprocess_tee import run as _run
 17try:
 18    import loggy
 19except ImportError:
 20    from utils import loggy
 21try:
 22    from .common import resolve_gocd_pipeline_variable
 23except ImportError:
 24    from utils.common import resolve_gocd_pipeline_variable
 25
 26
 27from enum import Enum
 28
 29
 30class Release(Enum):
 31    DEV = "develop"
 32    DEVELOP = "develop"
 33    QA = "qa"
 34    STAGE = "staging"
 35    STAGING = "staging"
 36    DEMO = "demo"
 37    PROD = "master"
 38    HOTFIX = "hotfix"
 39
 40
 41def get_semver():
 42    """
 43    get_semver()
 44
 45    Generates a new build version string using Semantic Versioning using environment variables.
 46
 47    Uses the following Environment variables to generate the version string:
 48
 49    * VERSION_MAJOR
 50    * VERSION_MINOR
 51    * GO_PIPELINE_COUNTER
 52
 53    Returns: None if environment variables are not set
 54    """
 55    if os.environ.get('VERSION_MAJOR') and os.environ.get('VERSION_MINOR') and os.environ.get('GO_PIPELINE_COUNTER'):
 56        return f"{os.environ.get('VERSION_MAJOR')}.{os.environ.get('VERSION_MINOR')}.{os.environ.get('GO_PIPELINE_COUNTER')}"
 57
 58    return None
 59
 60
 61def get_version():
 62    """
 63    get_version()
 64
 65    Generates a new build version string using `get_new_build_release()`
 66
 67    NOTE: This is the `default` version format for ALL of our backend versioning.
 68
 69    """
 70    return get_new_build_release()
 71
 72
 73def get_new_build_release():
 74    """
 75    get_new_build_release()
 76
 77    Generates a new build version string in the format: YY.MM.DD.GO_PIPELINE_COUNTER
 78
 79    returns: String
 80    """
 81    now = datetime.datetime.now().strftime("%y.%m.%d.")
 82    version = f"{now}{os.environ.get('GO_PIPELINE_COUNTER', '0')}"
 83    loggy.info(f"release.get_new_build_release(): Generated build: {version}")
 84    return version
 85
 86
 87def _check_for_multiple_materials():
 88    """
 89    _check_for_multiple_materials()
 90
 91    Checks for and exits if multiple materials in a GoCD pipeline are found.
 92    We currently do not support this in our release functions.
 93
 94    returns: False
 95    """
 96    counter = sum(x.startswith('GO_REVISION_') for x in os.environ.keys())
 97
 98    if counter > 1:
 99        loggy.error("release.get_commit_short_hash(): Error: (" + counter + ") Materials found in Env. We only support 1 Material right now.")
100        sys.exit(1)
101
102    return False
103
104
105def get_commit_short_hash():
106    """
107    get_commit_short_hash()
108
109    Grabs the git commit hash from ENV vars in a GoCD pipeline and returns the first 8 chars.
110
111    returns: String
112    """
113    _check_for_multiple_materials()
114    return next(val for key, val in os.environ.items() if key.startswith('GO_REVISION_'))[0:8]
115
116
117def get_source_branch():
118    """
119    get_source_branch()
120
121    Grabs the git branch from ENV vars in a GoCD pipeline
122
123    returns: String
124    """
125    _check_for_multiple_materials()
126    try:
127        return next(val for key, val in os.environ.items() if key.startswith('GO_MATERIAL_BRANCH'))
128    except StopIteration as e:
129        loggy.error(f"release.get_source_branch(): No branches found. GO_MATERIAIL_BRANCH_x env vars not found. Is this a GoCD pipeline? {str(e)}")
130        return None
131
132
133def get_last_tag():
134    """
135    get_last_tag()
136
137    Grabs the latest tag in a git repo.
138
139    returns: String or None
140    """
141    _run("git fetch --all --tags", check=False, shell=True)
142    latest_tag = _run("git for-each-ref refs/tags --sort=-taggerdate --count=1 --format=\"%(refname)\"", check=False, shell=True)
143
144    resolved = latest_tag.stdout.strip().split('/')[-1] if latest_tag else None
145
146    loggy.info(f"release.get_last_tag(): Returning latest tag as: {resolved}")
147    return resolved
148
149
150def git_promote(version=None, source=None, dest=None, tag=None) -> bool:
151    """
152    git_promote()
153
154    Git promote a release from one branch to another.
155
156    version: Defaults to `get_new_build_release()`, which generates a version
157    string in the following format: YYYY.MM.BUILD_NUMBER
158
159    source: Defaults to `develop`
160
161    dest: Defaults to `qa`
162
163    tag: Default is None. If set, we will tag the release version.
164    """
165    source_branch = 'develop' if source is None else resolve_gocd_pipeline_variable(source)
166    dest_branch = 'qa' if dest is None else resolve_gocd_pipeline_variable(dest)
167    version = get_new_build_release() if version is None else resolve_gocd_pipeline_variable(version)
168    promote_branch = f"promote/{version}"
169
170    _run("git status", check=False, shell=True)
171    _run("git config -l", check=False, shell=True)
172    _skip_merge = False
173
174    try:
175        _run(f"git checkout {dest_branch}", check=True, shell=True)
176        _run(f"git pull origin {dest_branch}", check=True, shell=True)
177    except Exception as e:
178        # dest branch doesn't exist. creating it
179        loggy.error("release.git_promote(): Exception: " + str(e))
180        _run(f"git checkout -b {dest_branch}", check=True, shell=True)
181        _run(f"git push origin {dest_branch}", check=True, shell=True)
182
183        _skip_merge = True
184        pass
185
186    if not _skip_merge:
187        _run(f"git checkout {source_branch}", check=True, shell=True)
188        _run(f"git pull origin {source_branch}", check=True, shell=True)
189
190        try:
191            loggy.info(f"release.git_promote(): Creating temp release branch {promote_branch}")
192            _run(f"git checkout -b {promote_branch}", check=True, shell=True)
193        except Exception as e:
194            # branch probably already exists...
195            loggy.error("release.git_promote(): Exception: " + str(e))
196            _run(f"git branch -D {promote_branch}", check=True, shell=True)
197            _run(f"git checkout -b {promote_branch}", check=True, shell=True)
198            pass
199
200        _run(f"git merge --strategy=ours --no-edit {dest_branch}", check=True, shell=True)
201        _run(f"git checkout {dest_branch}", check=True, shell=True)
202        _run(f"git merge --squash {promote_branch}", check=True, shell=True)
203
204        try:
205            output = _run(f"git commit -m \"GoCD merge {source_branch} to {dest_branch} for promotion {version}\"", check=False, shell=True)
206
207            if output.returncode != 0:
208                if 'nothing to commit' in output.stderr or 'nothing to commit' in output.stdout:
209                    loggy.info("release.git_commit(): Nothing to commit. Skipping push and proceeding as success.")
210                else:
211                    loggy.error("release.git_commit(): Git commit failure. Exiting. now")
212                    loggy.error(f"release.git_promote(): {output.stdout}")
213                    loggy.error(f"release.git_promote(): {output.stderr}")
214                    loggy.error(f"release.git_promote(): {str(output.returncode)}")
215                    raise Exception(output.stderr)
216            else:
217                _run(f"git push origin {dest_branch}", check=True, shell=True)
218        except Exception as e:
219            loggy.error(f"release.git_promot(): {str(e)}")
220            return False
221
222    if 'master' in dest_branch:
223        version = get_new_build_release() if tag is None else tag
224        _run(f"git tag -f -a {version} -m \"Rev to version {version}\"", check=True, shell=True)
225        _run(f"git push origin {version}", check=True, shell=True)
226
227    return True
class Release(enum.Enum):
31class Release(Enum):
32    DEV = "develop"
33    DEVELOP = "develop"
34    QA = "qa"
35    STAGE = "staging"
36    STAGING = "staging"
37    DEMO = "demo"
38    PROD = "master"
39    HOTFIX = "hotfix"

An enumeration.

DEV = <Release.DEV: 'develop'>
DEVELOP = <Release.DEV: 'develop'>
QA = <Release.QA: 'qa'>
STAGE = <Release.STAGE: 'staging'>
STAGING = <Release.STAGE: 'staging'>
DEMO = <Release.DEMO: 'demo'>
PROD = <Release.PROD: 'master'>
HOTFIX = <Release.HOTFIX: 'hotfix'>
Inherited Members
enum.Enum
name
value
def get_semver()
42def get_semver():
43    """
44    get_semver()
45
46    Generates a new build version string using Semantic Versioning using environment variables.
47
48    Uses the following Environment variables to generate the version string:
49
50    * VERSION_MAJOR
51    * VERSION_MINOR
52    * GO_PIPELINE_COUNTER
53
54    Returns: None if environment variables are not set
55    """
56    if os.environ.get('VERSION_MAJOR') and os.environ.get('VERSION_MINOR') and os.environ.get('GO_PIPELINE_COUNTER'):
57        return f"{os.environ.get('VERSION_MAJOR')}.{os.environ.get('VERSION_MINOR')}.{os.environ.get('GO_PIPELINE_COUNTER')}"
58
59    return None

get_semver()

Generates a new build version string using Semantic Versioning using environment variables.

Uses the following Environment variables to generate the version string:

  • VERSION_MAJOR
  • VERSION_MINOR
  • GO_PIPELINE_COUNTER

Returns: None if environment variables are not set

def get_version()
62def get_version():
63    """
64    get_version()
65
66    Generates a new build version string using `get_new_build_release()`
67
68    NOTE: This is the `default` version format for ALL of our backend versioning.
69
70    """
71    return get_new_build_release()

get_version()

Generates a new build version string using get_new_build_release()

NOTE: This is the default version format for ALL of our backend versioning.

def get_new_build_release()
74def get_new_build_release():
75    """
76    get_new_build_release()
77
78    Generates a new build version string in the format: YY.MM.DD.GO_PIPELINE_COUNTER
79
80    returns: String
81    """
82    now = datetime.datetime.now().strftime("%y.%m.%d.")
83    version = f"{now}{os.environ.get('GO_PIPELINE_COUNTER', '0')}"
84    loggy.info(f"release.get_new_build_release(): Generated build: {version}")
85    return version

get_new_build_release()

Generates a new build version string in the format: YY.MM.DD.GO_PIPELINE_COUNTER

returns: String

def get_commit_short_hash()
106def get_commit_short_hash():
107    """
108    get_commit_short_hash()
109
110    Grabs the git commit hash from ENV vars in a GoCD pipeline and returns the first 8 chars.
111
112    returns: String
113    """
114    _check_for_multiple_materials()
115    return next(val for key, val in os.environ.items() if key.startswith('GO_REVISION_'))[0:8]

get_commit_short_hash()

Grabs the git commit hash from ENV vars in a GoCD pipeline and returns the first 8 chars.

returns: String

def get_source_branch()
118def get_source_branch():
119    """
120    get_source_branch()
121
122    Grabs the git branch from ENV vars in a GoCD pipeline
123
124    returns: String
125    """
126    _check_for_multiple_materials()
127    try:
128        return next(val for key, val in os.environ.items() if key.startswith('GO_MATERIAL_BRANCH'))
129    except StopIteration as e:
130        loggy.error(f"release.get_source_branch(): No branches found. GO_MATERIAIL_BRANCH_x env vars not found. Is this a GoCD pipeline? {str(e)}")
131        return None

get_source_branch()

Grabs the git branch from ENV vars in a GoCD pipeline

returns: String

def get_last_tag()
134def get_last_tag():
135    """
136    get_last_tag()
137
138    Grabs the latest tag in a git repo.
139
140    returns: String or None
141    """
142    _run("git fetch --all --tags", check=False, shell=True)
143    latest_tag = _run("git for-each-ref refs/tags --sort=-taggerdate --count=1 --format=\"%(refname)\"", check=False, shell=True)
144
145    resolved = latest_tag.stdout.strip().split('/')[-1] if latest_tag else None
146
147    loggy.info(f"release.get_last_tag(): Returning latest tag as: {resolved}")
148    return resolved

get_last_tag()

Grabs the latest tag in a git repo.

returns: String or None

def git_promote(version=None, source=None, dest=None, tag=None) -> bool:
151def git_promote(version=None, source=None, dest=None, tag=None) -> bool:
152    """
153    git_promote()
154
155    Git promote a release from one branch to another.
156
157    version: Defaults to `get_new_build_release()`, which generates a version
158    string in the following format: YYYY.MM.BUILD_NUMBER
159
160    source: Defaults to `develop`
161
162    dest: Defaults to `qa`
163
164    tag: Default is None. If set, we will tag the release version.
165    """
166    source_branch = 'develop' if source is None else resolve_gocd_pipeline_variable(source)
167    dest_branch = 'qa' if dest is None else resolve_gocd_pipeline_variable(dest)
168    version = get_new_build_release() if version is None else resolve_gocd_pipeline_variable(version)
169    promote_branch = f"promote/{version}"
170
171    _run("git status", check=False, shell=True)
172    _run("git config -l", check=False, shell=True)
173    _skip_merge = False
174
175    try:
176        _run(f"git checkout {dest_branch}", check=True, shell=True)
177        _run(f"git pull origin {dest_branch}", check=True, shell=True)
178    except Exception as e:
179        # dest branch doesn't exist. creating it
180        loggy.error("release.git_promote(): Exception: " + str(e))
181        _run(f"git checkout -b {dest_branch}", check=True, shell=True)
182        _run(f"git push origin {dest_branch}", check=True, shell=True)
183
184        _skip_merge = True
185        pass
186
187    if not _skip_merge:
188        _run(f"git checkout {source_branch}", check=True, shell=True)
189        _run(f"git pull origin {source_branch}", check=True, shell=True)
190
191        try:
192            loggy.info(f"release.git_promote(): Creating temp release branch {promote_branch}")
193            _run(f"git checkout -b {promote_branch}", check=True, shell=True)
194        except Exception as e:
195            # branch probably already exists...
196            loggy.error("release.git_promote(): Exception: " + str(e))
197            _run(f"git branch -D {promote_branch}", check=True, shell=True)
198            _run(f"git checkout -b {promote_branch}", check=True, shell=True)
199            pass
200
201        _run(f"git merge --strategy=ours --no-edit {dest_branch}", check=True, shell=True)
202        _run(f"git checkout {dest_branch}", check=True, shell=True)
203        _run(f"git merge --squash {promote_branch}", check=True, shell=True)
204
205        try:
206            output = _run(f"git commit -m \"GoCD merge {source_branch} to {dest_branch} for promotion {version}\"", check=False, shell=True)
207
208            if output.returncode != 0:
209                if 'nothing to commit' in output.stderr or 'nothing to commit' in output.stdout:
210                    loggy.info("release.git_commit(): Nothing to commit. Skipping push and proceeding as success.")
211                else:
212                    loggy.error("release.git_commit(): Git commit failure. Exiting. now")
213                    loggy.error(f"release.git_promote(): {output.stdout}")
214                    loggy.error(f"release.git_promote(): {output.stderr}")
215                    loggy.error(f"release.git_promote(): {str(output.returncode)}")
216                    raise Exception(output.stderr)
217            else:
218                _run(f"git push origin {dest_branch}", check=True, shell=True)
219        except Exception as e:
220            loggy.error(f"release.git_promot(): {str(e)}")
221            return False
222
223    if 'master' in dest_branch:
224        version = get_new_build_release() if tag is None else tag
225        _run(f"git tag -f -a {version} -m \"Rev to version {version}\"", check=True, shell=True)
226        _run(f"git push origin {version}", check=True, shell=True)
227
228    return True

git_promote()

Git promote a release from one branch to another.

version: Defaults to get_new_build_release(), which generates a version string in the following format: YYYY.MM.BUILD_NUMBER

source: Defaults to develop

dest: Defaults to qa

tag: Default is None. If set, we will tag the release version.