├── .dockerignore ├── .gitignore ├── Dockerfile ├── Jenkinsfile ├── README.md ├── demo.gif ├── install.sh ├── integration ├── basic.sh └── runner.sh ├── requirements.txt ├── tests ├── __init__.py └── test_verpy.py ├── verpy.py ├── verpy ├── __init__.py ├── file_ops.py ├── version.py └── version.template.py └── version.py /.dockerignore: -------------------------------------------------------------------------------- 1 | venv/ 2 | .coverage 3 | *.swp 4 | install.sh 5 | __pycache__/ 6 | README.md 7 | *.pyc 8 | .git/ 9 | .gitignore 10 | demo.gif 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .coverage 2 | venv/ 3 | __pycache__/ 4 | .cache/ 5 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:alpine3.6 2 | 3 | WORKDIR /usr/src/verpy 4 | COPY . . 5 | 6 | RUN pip install -r requirements.txt 7 | 8 | WORKDIR /usr/src/verpy/destination 9 | 10 | ENTRYPOINT ["python", "../verpy.py"] 11 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | pipeline { 2 | agent any 3 | environment { 4 | BUILD_VERSION = '$(python3 -c \'from version import VERSION; print(VERSION,end="")\')' 5 | DOCKER_REPOSITORY = 'trstringer/verpy' 6 | } 7 | stages { 8 | stage('Build') { 9 | steps { 10 | echo "building..." 11 | sh "docker build -t ${env.DOCKER_REPOSITORY}:${env.BUILD_VERSION} -t ${env.DOCKER_REPOSITORY}:latest ." 12 | echo "installing virtual environment" 13 | sh "python3 -m venv venv" 14 | sh ". venv/bin/activate && pip install -r requirements.txt" 15 | } 16 | } 17 | stage('Unit Tests') { 18 | steps { 19 | sh ". venv/bin/activate && pytest" 20 | } 21 | } 22 | stage('Integration Tests') { 23 | steps { 24 | sh ". venv/bin/activate && integration/runner.sh" 25 | sh "docker run --rm -v \$(pwd):/usr/src/verpy/destination trstringer/verpy:latest --version" 26 | } 27 | } 28 | stage('Deliver') { 29 | when { 30 | expression { 31 | currentBuild.result == null || currentBuild.result == 'SUCCESS' 32 | } 33 | } 34 | steps { 35 | echo "pushing image to container registry..." 36 | sh "docker push ${env.DOCKER_REPOSITORY}:${env.BUILD_VERSION}" 37 | sh "docker push ${env.DOCKER_REPOSITORY}:latest" 38 | } 39 | } 40 | } 41 | post { 42 | always { 43 | sh "docker image prune -f" 44 | } 45 | failure { 46 | mail bcc: '', body: 'verpy build failed', cc: '', from: '', replyTo: '', subject: ' Build failed', to: 'github@trstringer.com' 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Verpy 2 | 3 | Python application versioning tool 4 | 5 | ![Demo](demo.gif) 6 | 7 | ## Setup 8 | 9 | This is a Docker-native application (i.e. I developed this with the conscious effort to run it as a container). With that being said, to run it in Docker you'll of course have to have Docker installed and running. 10 | 11 | To create the necessary components, simply from `$ . install.sh` from the root of the repo. 12 | 13 | *Note: I'm sure with very little work you could get this to run in a virtual environment or (not advised) outside of a virtual environment. If there is interest in this, I would create the script or (better yet) accept a pull request.* 14 | 15 | ## Usage 16 | 17 | - Initialize the version file (`version.py`): `$ verpy init` 18 | - Display the current version: `$ verpy version` 19 | - Increment the *major* component of the version: `$ verpy version major` 20 | - Increment the *minor* component of the version: `$ verpy version minor` 21 | - Increment the *patch* component of the version: `$ verpy version patch` 22 | 23 | - Display help: `$ verpy --help` or `$ verpy` 24 | - Display the Verpy version: `$ verpy --version` 25 | 26 | 27 | ## Updates 28 | 29 | This happens automatically. If you run `$ type verpy` you'll see that the shell function does a `docker pull`. 30 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trstringer/verpy/eabbf4c63d9f58551fe1d22f936d6868fb90807b/demo.gif -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | setup() { 4 | VERPY_DEF=" 5 | verpy() { 6 | docker pull trstringer/verpy:latest >> /dev/null 7 | docker run --rm \\ 8 | -v \$(pwd):/usr/src/verpy/destination \\ 9 | trstringer/verpy:latest \"\$@\" 10 | 11 | if [[ -f version.py ]]; then 12 | sudo chown \$USER:\$USER version.py 13 | fi 14 | }" 15 | grep "source ~/.verpy" ~/.bashrc >> /dev/null 16 | if [ $? -ne 0 ]; then 17 | printf "adding .verpy sourcing to bashrc..." 18 | printf "\nsource ~/.verpy" >> ~/.bashrc 19 | else 20 | printf ".verpy sourcing already in bashrc..." 21 | fi 22 | 23 | printf "$VERPY_DEF" > ~/.verpy 24 | . ~/.bashrc 25 | } 26 | 27 | setup 28 | -------------------------------------------------------------------------------- /integration/basic.sh: -------------------------------------------------------------------------------- 1 | validate_version() { 2 | if [[ "$1" == "$2" ]]; then 3 | return 0 4 | else 5 | return 1 6 | fi 7 | } 8 | 9 | mkdir integration/test_dir 10 | 11 | . venv/bin/activate 12 | 13 | cd integration/test_dir 14 | 15 | echo running init integration test 16 | python ../../verpy.py init 17 | 18 | if [[ ! -f version.py ]]; then 19 | echo version file expected but not existing 20 | exit 1 21 | fi 22 | 23 | echo displaying current version 24 | python ../../verpy.py version 25 | 26 | if [[ ! -f version.py ]]; then 27 | echo displaying current version failed 28 | exit 1 29 | fi 30 | 31 | echo running version major increment integration test 32 | python ../../verpy.py version major 33 | 34 | if [[ $? -ne 0 ]]; then 35 | echo failed incrementing version major 36 | exit 1 37 | else 38 | VERSION_OUTPUT=$(python ../../verpy.py version) 39 | echo $VERSION_OUTPUT 40 | EXPECTED="2.0.0" 41 | validate_version "$VERSION_OUTPUT" "$EXPECTED" 42 | if [[ $? -ne 0 ]]; then 43 | echo expected $EXPECTED but got $VERSION_OUTPUT 44 | exit 1 45 | fi 46 | fi 47 | 48 | echo running version minor increment integration test 49 | python ../../verpy.py version minor 50 | 51 | if [[ $? -ne 0 ]]; then 52 | echo failed incrementing version minor 53 | exit 1 54 | else 55 | VERSION_OUTPUT=$(python ../../verpy.py version) 56 | echo $VERSION_OUTPUT 57 | EXPECTED="2.1.0" 58 | validate_version "$VERSION_OUTPUT" "$EXPECTED" 59 | if [[ $? -ne 0 ]]; then 60 | echo expected $EXPECTED but got $VERSION_OUTPUT 61 | exit 1 62 | fi 63 | fi 64 | 65 | echo running version patch increment integration test 66 | python ../../verpy.py version patch 67 | 68 | if [[ $? -ne 0 ]]; then 69 | echo failed incrementing version patch 70 | exit 1 71 | else 72 | VERSION_OUTPUT=$(python ../../verpy.py version) 73 | echo $VERSION_OUTPUT 74 | EXPECTED="2.1.1" 75 | validate_version "$VERSION_OUTPUT" "$EXPECTED" 76 | if [[ $? -ne 0 ]]; then 77 | echo expected $EXPECTED but got $VERSION_OUTPUT 78 | exit 1 79 | fi 80 | fi 81 | 82 | exit 0 83 | -------------------------------------------------------------------------------- /integration/runner.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cleanup() { 4 | rm -rf integration/test_dir 5 | } 6 | 7 | # run a basic integration test start to finish 8 | ./integration/basic.sh 9 | EXIT_STATUS=$? 10 | 11 | if [[ $EXIT_STATUS -ne 0 ]]; then 12 | echo failed running basic integration test 13 | cleanup 14 | exit $EXIT_STATUS 15 | fi 16 | 17 | cleanup 18 | exit 0 19 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | astroid==1.5.3 2 | coverage==4.4.1 3 | isort==4.2.15 4 | lazy-object-proxy==1.3.1 5 | mccabe==0.6.1 6 | py==1.4.34 7 | pylint==1.7.4 8 | pytest==3.2.3 9 | pytest-cov==2.5.1 10 | six==1.11.0 11 | wrapt==1.10.11 12 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trstringer/verpy/eabbf4c63d9f58551fe1d22f936d6868fb90807b/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_verpy.py: -------------------------------------------------------------------------------- 1 | """Test the main verpy code""" 2 | 3 | from verpy import Version 4 | 5 | def test_init(): 6 | """Test version initialization""" 7 | 8 | version = Version() 9 | 10 | assert version.major == 1, 'major version should initialize correctly' 11 | assert version.minor == 0, 'minor version should initialize correctly' 12 | assert version.patch == 0, 'patch version should initialize correctly' 13 | 14 | def test_parse_version(): 15 | """Test version parsing""" 16 | 17 | version_initial = '1.2.3' 18 | version_final = Version.parse(version_initial) 19 | assert version_final.major == 1, 'major version parse matching' 20 | assert version_final.minor == 2, 'minor version parse matching' 21 | assert version_final.patch == 3, 'patch version parse matching' 22 | 23 | def test_increment_version_major(): 24 | """Test version major increment""" 25 | 26 | version_initial = '1.2.3' 27 | version_final = Version.parse(version_initial).increment_major() 28 | assert version_final.major == 2, 'major version should increment' 29 | assert version_final.minor == 0, 'minor version should be zero\'d out' 30 | assert version_final.patch == 0, 'patch version should be zero\'d out' 31 | 32 | def test_increment_version_minor(): 33 | """Test version minor increment""" 34 | 35 | version_initial = '1.2.3' 36 | version_final = Version.parse(version_initial).increment_minor() 37 | assert version_final.major == 1, 'major version should stay the same' 38 | assert version_final.minor == 3, 'minor version should increment' 39 | assert version_final.patch == 0, 'patch version should be zero\'d out' 40 | 41 | def test_increment_version_patch(): 42 | """Test version patch increment""" 43 | 44 | version_initial = '1.2.3' 45 | version_final = Version.parse(version_initial).increment_patch() 46 | assert version_final.major == 1, 'major version should stay the same' 47 | assert version_final.minor == 2, 'minor version should stay the same' 48 | assert version_final.patch == 4, 'patch version should increment' 49 | -------------------------------------------------------------------------------- /verpy.py: -------------------------------------------------------------------------------- 1 | """Main verpy code module""" 2 | 3 | import sys 4 | from verpy import file_ops 5 | from version import VERSION 6 | 7 | def display_app_help(): 8 | """Display app usage""" 9 | 10 | print('verpy Usage:') 11 | print() 12 | print(' verpy init initialize versioning for current directory') 13 | print(' verpy version display the current version') 14 | print(' verpy version {major,minor,patch} increment version section') 15 | print() 16 | print(' Global params:') 17 | print(' --help display application help') 18 | print(' --version display application version') 19 | 20 | def initialize_current_directory(): 21 | """Initialize the versioning in the current directory""" 22 | 23 | print(file_ops.initialize_version_file()) 24 | 25 | def display_or_change_version(cli_args_version_info): 26 | """Display or change the current working directory version""" 27 | 28 | if cli_args_version_info['major']: 29 | print(file_ops.increment_version_major()) 30 | elif cli_args_version_info['minor']: 31 | print(file_ops.increment_version_minor()) 32 | elif cli_args_version_info['patch']: 33 | print(file_ops.increment_version_patch()) 34 | else: 35 | print(file_ops.get_current_version()) 36 | 37 | def args(): 38 | """Handle command line arguments""" 39 | 40 | root_cmd = '--help' 41 | try: 42 | root_cmd = sys.argv[1].lower() 43 | except IndexError: 44 | pass 45 | 46 | second_cmd = None 47 | try: 48 | second_cmd = sys.argv[2] 49 | except IndexError: 50 | pass 51 | 52 | cmd_matrix = dict( 53 | app_version=root_cmd == '--version', 54 | app_help=root_cmd == '--help', 55 | init=root_cmd == 'init', 56 | version=dict( 57 | major=second_cmd == 'major', 58 | minor=second_cmd == 'minor', 59 | patch=second_cmd == 'patch' 60 | ) if root_cmd == 'version' else False 61 | ) 62 | 63 | return cmd_matrix 64 | 65 | def main(): 66 | """Main code execution""" 67 | 68 | cli_args = args() 69 | 70 | if cli_args['app_help']: 71 | display_app_help() 72 | elif cli_args['app_version']: 73 | print('verpy v{}'.format(VERSION)) 74 | elif cli_args['version']: 75 | display_or_change_version(cli_args['version']) 76 | elif cli_args['init']: 77 | initialize_current_directory() 78 | 79 | if __name__ == '__main__': 80 | main() 81 | -------------------------------------------------------------------------------- /verpy/__init__.py: -------------------------------------------------------------------------------- 1 | """Init imports""" 2 | 3 | from .version import Version 4 | -------------------------------------------------------------------------------- /verpy/file_ops.py: -------------------------------------------------------------------------------- 1 | """Version file operations""" 2 | 3 | import os 4 | import re 5 | from verpy.version import Version 6 | 7 | def version_filename(): 8 | """Retrieve the path and filename for the destination version file""" 9 | 10 | return os.path.join(os.getcwd(), 'version.py') 11 | 12 | def write_version_to_file(version_to_write): 13 | """Write version to file""" 14 | 15 | template_filename = os.path.join( 16 | os.path.dirname(os.path.abspath(__file__)), 17 | 'version.template.py' 18 | ) 19 | 20 | with open(template_filename, 'r') as template_file: 21 | template_content = ''.join(template_file.readlines()) 22 | 23 | template_content = template_content.replace('VERSION_TO_WRITE', str(version_to_write)) 24 | version_filename_cache = version_filename() 25 | 26 | with open(version_filename_cache, 'w') as destination_file: 27 | destination_file.writelines(template_content) 28 | 29 | return str(version_to_write) 30 | 31 | def initialize_version_file(): 32 | """Create the base version file""" 33 | 34 | # initialize with base version (the Version class defaults) 35 | return write_version_to_file(Version()) 36 | 37 | def parse_version_from_file(): 38 | """Parse the version from the current file""" 39 | 40 | with open(version_filename(), 'r') as version_file: 41 | version_file_content = ''.join(version_file.readlines()) 42 | 43 | return re.search(r'(\d+\.\d+\.\d+)', version_file_content).group(1) 44 | 45 | def increment_version_major(): 46 | """Increment the major version""" 47 | 48 | parsed_version = Version.parse(parse_version_from_file()) 49 | new_version = parsed_version.increment_major() 50 | write_version_to_file(new_version) 51 | return str(new_version) 52 | 53 | def increment_version_minor(): 54 | """Increment the minor version""" 55 | 56 | parsed_version = Version.parse(parse_version_from_file()) 57 | new_version = parsed_version.increment_minor() 58 | write_version_to_file(new_version) 59 | return str(new_version) 60 | 61 | def increment_version_patch(): 62 | """Increment the patch version""" 63 | 64 | parsed_version = Version.parse(parse_version_from_file()) 65 | new_version = parsed_version.increment_patch() 66 | write_version_to_file(new_version) 67 | return str(new_version) 68 | 69 | def get_current_version(): 70 | """Get the current version stored in the file""" 71 | 72 | return Version.parse(parse_version_from_file()) 73 | -------------------------------------------------------------------------------- /verpy/version.py: -------------------------------------------------------------------------------- 1 | """Version class definition""" 2 | 3 | import re 4 | 5 | class Version: 6 | """This class stores version information and handles necessary parsing""" 7 | 8 | def __init__(self, major=1, minor=0, patch=0): 9 | """Initialize the object""" 10 | 11 | self.major = major 12 | self.minor = minor 13 | self.patch = patch 14 | 15 | @staticmethod 16 | def parse(version): 17 | """Parse the Version from a version string""" 18 | 19 | version_re_match = re.search(r'^(\d+)\.(\d+)\.(\d+)$', version) 20 | return Version( 21 | major=int(version_re_match.group(1)), 22 | minor=int(version_re_match.group(2)), 23 | patch=int(version_re_match.group(3)) 24 | ) 25 | 26 | def increment_major(self): 27 | """Increment the major part of the version""" 28 | 29 | return Version( 30 | major=self.major + 1, 31 | minor=0, 32 | patch=0 33 | ) 34 | 35 | def increment_minor(self): 36 | """Increment the minor part of the version""" 37 | 38 | return Version( 39 | major=self.major, 40 | minor=self.minor + 1, 41 | patch=0 42 | ) 43 | 44 | def increment_patch(self): 45 | """Increment the patch part of the version""" 46 | 47 | return Version( 48 | major=self.major, 49 | minor=self.minor, 50 | patch=self.patch + 1 51 | ) 52 | 53 | def __str__(self): 54 | """Format the Version to a string""" 55 | 56 | return '{}.{}.{}'.format( 57 | self.major, 58 | self.minor, 59 | self.patch 60 | ) 61 | -------------------------------------------------------------------------------- /verpy/version.template.py: -------------------------------------------------------------------------------- 1 | """Application versioning""" 2 | 3 | # DO NOT MODIFY!!! 4 | # 5 | # This module has been generated by and is currently maintained 6 | # from verpy. To find out how to change the versioning information 7 | # please refer to `verpy --help` for usage 8 | 9 | VERSION = 'VERSION_TO_WRITE' 10 | -------------------------------------------------------------------------------- /version.py: -------------------------------------------------------------------------------- 1 | """Application versioning""" 2 | 3 | # DO NOT MODIFY!!! 4 | # 5 | # This module has been generated by and is currently maintained 6 | # from verpy. To find out how to change the versioning information 7 | # please refer to `verpy --help` for usage 8 | 9 | VERSION = '1.0.3' 10 | --------------------------------------------------------------------------------