In Eliminating Failed Deployments – Part 2 – Automate Your Obsession, one of the checks I suggested was:
Ensure all of the binary files have an appropriate version number; “1.0.0.0” is NOT an appropriate version number.
So, WHY isn’t 1.0.0.0, or anything like that, an appropriate version?
It’s because it’s artificial – a dummy value that’s normally used because the developers haven’t implemented automatic versioning on the build system. If you’re in this situation you need to change it. If you need reasons, there are several:
Optimism – It Can Make Things Go Right!
Both Windows and Linux are designed to handle versioned binary files and distribution packages and installers very well. If you try to install a new version of something over an older version, the system handles everything for you.
If you try to install something with the same version number as the package that is already installed, Windows at least will start complaining, and you have to work harder to upgrade the files. If you’re using continuous integration, continuous deployment and/or DevOps, you don’t want to do anything that makes deployments more difficult.
Incrementing the version number of the files makes your deployment process easier and more reliable – certainly a couple of arguments in its favour.
I’ve spoken to people who don’t see these as reasons to increment version numbers with each release, but their arguments have never really seemed convincing. Even if you recreate a VM image with every release, you still need to know where your code came from.
Unique version numbers also give you greater traceability, which is always good. Without that, you get the problem described in the next section.
Pessimism – It’s Going To Go Wrong!
Imagine you have a bug reported in your production system. You trace it to one subsystem, and then decide that you need to make a change to the source code to fix it. How do you know where to find the source code for that version so that you can change it? There’s a very good chance the version in production isn’t the latest version in your source control system. You might have to dig round in the archive until you find the right version.
It’s a pain, but if every build has a unique version, and your source control is labelled or identified in some other way, it is at least possible.
Now imagine everything is version 1.0.0.0. How will you find the right version of the source code? You may be lucky and be able to see the date the system was compiled. As long as your CI system didn’t produce lots of builds around the same time, and assuming there isn’t a time zone difference involved anywhere to add to the confusion.
You may be able to get some clues from labels in the version control system, but that may or may not work.
If you get really desperate you may even have to resort to decompiling executables and libraries, as long as they’re in a language where you can actually decompile them to something resembling the original source code. Then you can compare the decompiled executables and libraries to various versions of your source code, all the while wondering where you went wrong to get saddled with sorting out this mess. If you can’t decompile the binary files then you’ll just have to make an informed guess about the version of source code you need.
This is a situation that should never happen in a professional environment, under any circumstances. That doesn’t mean it doesn’t happen, but it shouldn’t.
Realism – It’s Not Difficult!
To implement automatic versioning in your build system you need to do the following:
- Obtain the current build number. This is something that the build server (certainly Jenkins and TeamCity) will provide easily, usually as a macro or variable that you can use in command lines or scripts.
- Update the version number in your source code. Ideally this would be in one place, but it may be in several especially if you’re developing in several languages.
- Carry out the build for your system
- Update the version control system with the latest version number – updating the actual source files or adding a label will work.
You’ll probably have to do a little scripting to do this, but there are plenty of examples of how to do this freely available on the web. It’s only got to be done once per project, and the scripts generally don’t change.
To simplify things even more, if you’re using C# and you have a solution comprising multiple projects, you could remove the common information from all of the AssemblyInfo.cs
files (Company details, copyright, version specifications etc.) and place it in a new file called CommonAssemblyInfo.cs
in the same folder as the solution. Add this file to each project as a link, NOT a source file. Once you’ve done this, any changes to CommonAssemblyInfo.cs
will naturally appear in all of the modules.
Practicality – Choosing The Version Numbers
I mentioned above that build systems provide a build number that can be accessed quite easily, but that’s only one number. Where do the others come from, for example in the four part version numbers used by windows?
The documentation describing the way to version assemblies, on the MSDN at Assembly Versioning says that the structure is:
<major version>.<minor version>.<build number>.<revision>
There are two version numbers in .NET, AssemblyVersion and AssemblyFileVersion. The differences between them are explained at: How to use Assembly Version and Assembly File Version.
Alternatively for APIs the SemVer2 standard at https://semver.org is straightforward, logical and has helped at least one of my friends’ departments pass a development audit. It’s definitely worth considering.
An Example
The Python 3 script below can be run from the command prompt or from within a build system and will set the build numbers in the AssemblyInfo.cs or CommonAssemblyInfo.cs files in a given directory and its subdirectories:
{slider title="Expand Here To See The Code To Update Versions In C# Code" open="false" class="icon"}
import argparse import re from io import StringIO from os import walk from os.path import join, isdir # Copyright 2017 Jason Ross.
# Distributed under the MIT License class VersionNumberUpdater: @staticmethod def find_assembly_info_files(root_directory, file_ending='AssemblyInfo.cs'): """Iterates through the directory structure starting at the root and searching for files that may contain the version definitions for the solution. :param root_directory: The root directory of the search. This directory and all of its subdirectories are searched for files which may need to be updated. :param file_ending: The ending of the file names that will be added to the list of files that should be checked for version definitions. As most C# projects either contain a file called 'AssemblyInfo.cs', or refer to a common file called 'CommonAssemblyInfo.cs', this argument defaults to 'AssemblyInfo.cs' to match both names. :return: A collection of paths of files which should be checked for version specifications. """ files_to_check = [] for root, directories, files in walk(root_directory): for file_to_check in [filename for filename in files if filename.endswith(file_ending)]: files_to_check.append(join(root, file_to_check)) return files_to_check @staticmethod def update_version_numbers(file_paths, version_name, build_number): """Iterate through all of the given files, updating the version numbers contained within them. :param file_paths: A collection of paths of files which are to be checked for version specifications. :param version_name: Either 'AssemblyVersion' or 'AssemblyFileVersion' depending upon which of the Windows version attributes you want to change. :param build_number: The new build number to be placed into the overall version number. :return: The paths of the file(s) that were updated. """ version_pattern = '(?P\s*\[assembly\:' + \ version_name + \ '\(\"\d+\.\d+\.)(?P\d+)(?P\.\d+\"\))' version_regex = re.compile(version_pattern) updated_file_paths = [] # Get the contents of each of the files, and update the version numbers. for file_path in file_paths: version_updated = False # Create a memory buffer for the file. with StringIO() as buffer_file: # Read the source file into memory - there should be plenty of space. with open(file_path, mode='r') as file_object: for line_number, line_contents in enumerate(file_object): # Update the version numbers as the file contents are put into the memory file. match = version_regex.match(line_contents) if match: buffer_file.write( version_regex.sub(match.group('prefix') + str(build_number) + match.group('postfix'), line_contents)) version_updated = True else: buffer_file.write(line_contents) # Overwrite the original file with the contents of the memory buffer if the version was updated. if version_updated: buffer_file.seek(0) with open(file_path, mode='w') as file_object: file_object.writelines(buffer_file.readlines())
updated_file_paths.append(file_path) return updated_file_paths def main(): """Main function that carries out the updating of files that contain version declarations. Copyright 2017 Jason Ross :return: """ # Use the argparser module as it produces help automatically. parser = argparse.ArgumentParser(description='Update the version number in the source files in the root directory.') parser.add_argument('directory', help='The root directory of the search. This directory and all of its subdirectories ' 'are searched for files which may need to be updated.') parser.add_argument('buildnumber', help='The build number to be set in the version declarations.', type=int) parser.add_argument('-v', '--versionname', help='The type of version that will be updated by the script. This can be AssemblyVersion (a), ' 'AssemblyFileVersion(f) or both (b). Default is b.', choices=['a', 'f', 'b'], default='b') parser.add_argument('-f', '--fileending', help='The last part of the file to be matched to determine files that may contain version ' 'data. This is usually ''AssemblyInfo.cs'', but may be ''CommonAssemblyInfo.cs''. ' 'The default value is ''AssemblyInfo.cs''.', default='AssemblyInfo.cs') args = parser.parse_args() # Continue only if the directory exists. directory_path = args.directory if not isdir(directory_path): raise NotADirectoryError('The specified directory does not exist: ' + directory_path) # Continue only if the build number is valid if args.buildnumber < 0: raise ValueError('The build number must be larger than or equal to 0. The specified value was: ' + str(args.buildnumber)) # Set the remaining variables version_attribute_name = args.versionname # Scan the directories and find the files to check if args.fileending is None: assembly_info_files = VersionNumberUpdater.find_assembly_info_files(directory_path) else: assembly_info_files = VersionNumberUpdater.find_assembly_info_files(directory_path, args.fileending) # Update the version attributes as required. updated_files = [] if version_attribute_name in ['a', 'b']: updated_files.extend( VersionNumberUpdater.update_version_numbers(assembly_info_files, 'AssemblyVersion', args.buildnumber)) if version_attribute_name in ['f', 'b']: updated_files.extend( VersionNumberUpdater.update_version_numbers(assembly_info_files, 'AssemblyFileVersion', args.buildnumber)) # Get rid of duplicates - a file may have been updated twice, once for each version type: updated_files = sorted(list(set(updated_files))) # Let the caller know what was updated print('The following files were updated with build number ' + str(args.buildnumber) + ':') print('\n'.join(updated_files)) print('Finished') main()
{/sliders}
From a Windows command line, if you want to set the versions in the files in the directory "E:\Articles\Examples\Versioning" to 7434 this script can be run as:
python updateversions.py "E:\Articles\Examples\Versioning" 7434
Running:
python updateversions.py -h
will display the required and optional parameters for the script.This is only a basic script and, as I’m not exactly a Python expert, it took a few hours to write. It works though, and if you’d like to use it you’re welcome. I accept no responsibility if it doesn’t work, but if it does then please let me know.
The script shows that the small amount of effort it takes to implement automatically updated version numbers, and the advantages it gives, far outweighs the disadvantages of not bothering.