A little while back I stepped into a project where there was no automation. Releases were fairly haphazard and required a lot of tweaking to install into each deployment environment. A bigger issue was the amount of effort required to regenerate the .netTiers data/service layers when a column was added to a table. To be effective this stuff needs to be a push button exercise. In this project there was so much deferred maintenance on the templates it could take a day of manual tweaks to apply the necessary changes to the generated code before it would successfully compile - this was occurring instead of applying those changes to the actual templates that generated the code. And you should be familiar with how accurate and error-free tweaking generated code manually is right? I'm rambling, the moral of the story is that automation salves a myriad of ills and improves not only your ability to do your job but delivering installers that reliably install your application also vastly improves your relationship with the people responsible for looking after the servers your application runs on.
So this first post is to promote and for future reference record the sheer usefulness of PowerShell for getting your wares to where they need to be. Recently I've been refactoring a special application that keeps its data in XML files instead of, I don't know, an actual database? And for my first trick I need to separate one "manual" into two. Fortunately PowerShell knows XML in a very cool way. Time to look at a little code.
1: # update manuals.xml
2: function update_manual_xml {3: $docFile = Join-Path $APP_DATA "manuals.xml"4: Write-Host "Updating $docFile"5:
6: if (test-path $docFile)7: {
8: $doc = [xml](Get-Content $docFile)
9:
10: $eisaXml = $doc.manuals.manual | Where-Object { $_.code -eq "eisa" }11: $eiAdminEmail = $eisaXml.sections.section[0].GetAttribute("AdminEmail")12: $snAdminEmail = $eisaXml.sections.section[1].GetAttribute("AdminEmail")13:
14: [xml]$ei = '<manual AdminEmail="' + $eiAdminEmail + '" HideEffectiveDate="true">15: <code>ei</code>
16: <title>Manual Title</title>
17: <homePageText><![CDATA[Manual description]]></homePageText>
18: <sections>
19: <section>
20: <number>1</number>
21: <title>Section Title</title>
22: </section>
23: </sections>
24: </manual>'
25: [xml]$sn = '<manual AdminEmail="' + $snAdminEmail + '" HideEffectiveDate="true">26: <code>sn</code>
27: <title>Another Manual Title</title>
28: <homePageText><![CDATA[And description]]></homePageText>
29: <sections>
30: <section>
31: <number>1</number>
32: <title>Section Title</title>
33: </section>
34: </sections>
35: </manual>'
36: $eiMan = $doc.ImportNode($ei.manual, $true)37: $doc.manuals.AppendChild($eiMan)
38: $snMan = $doc.ImportNode($sn.manual, $true)39: $doc.manuals.AppendChild($snMan)
40: $doc.manuals.RemoveChild($eisaXml)
41: $doc.Save($docFile)
42: } else {43: Write-Host "File not found: $docFile"44: }
45: }
How great is that? Working with XML in PowerShell is easy. I'm not going to claim I'm writing great code here. More like just enough to get a one off upgrade sorted. The script should be largely self-explanatory but in case you’re after a little more detail I’ll go through the important bits. In line 3 I’m putting together the path to the manuals XML file using the Join-Path function. In this case the path is different for every location – in Test it’s on the C:\ drive, in Production it’s E:\ so for each case I load $APP_DATA to point to the website’s App_Data directory which is where the XML files are stored.Line 6, it’s always good to check the file exists before trying to use it. Then the meat course in line 8, load all that XML into something I can manipulate, using [xml] to convert the file contents into an XML DOM object. In line 10 I’m looking for an <eisa /> element which contains the two manuals I need to separate. In lines 14 and 25 I’m creating the XML for the new manuals. And finally lines 36 – 40 where I import the new manuals and remove the manual I’m replacing.
Installers
I also like to produce installers for each of the environments where I need to deploy an application. In my next post I'm going to document some MSBuild code to automate building an app for each configuration. Once those installers are built I use PowerShell to put them where the tech guys expect them to be, including creating the correct directory structure based on the application version number defined in Web.config:
1: $cwm = "D:\Work\AppRoot\"2: $cwmPackages = Join-Path $cwm "ProjectRoot\obj"3: $newVersions = "\\192.168.1.123\repo\"4: $environments = @{ Path = "Staging"; },5: @{ Path = "Development"; },6: @{ Path = "Test"; },7: @{ Path = "Production"; }8:
9: function get_version {
10: $webConfig = Join-Path $cwm "Web.config"11: Write-Host "Web.config $webConfig"12: if (-not(test-path $webConfig)) {
13: Write-Host("Can't locate Web.config")14: exit
15: }
16:
17: $configDoc = [xml](Get-Content $webConfig)
18: $webVersionXml = $configDoc.configuration.appSettings.add | Where-Object { $_.key -eq "Version" }
19: return $webVersionXml.GetAttribute("value")
20: }
21:
22: function create_new_version_folders {
23: $deployPath = Join-Path $newVersions $webVersion
24: if (-not(test-path $deployPath)) {
25: mkdir $deployPath > $null
26: }
27:
28: # create subfolders
29: foreach ($target in $environments) {
30: $targetPath = Join-Path $deployPath $target.Path
31: if (-not(test-path $targetPath)) {
32: mkdir $targetPath > $null
33: }
34: }
35:
36: return $deployPath
37: }
38:
39: function copy_package {
40: Param($deployPath)
41:
42: if (-not(test-path $cwmPackages)) {
43: Write-Host("Can't locate packages")44: exit
45: }
46:
47: foreach ($target in $environments) {
48: $sourcePath = Join-Path $cwmPackages ($target.Path + "\Package")49: $targetPath = Join-Path $deployPath $target.Path
50: $targetTemp = Join-Path $targetPath "\Package\PackageTmp"51:
52: Write-Host "Copying package to $targetPath"53: cp $sourcePath $targetPath -Force -recurse
54: rm $targetTemp -Force -recurse
55: }
56: }
57:
58: #
59: # Main
60: #
61: Write-Host "Using path $cwm"62: push-location $cwm
63:
64: # 1. Get version from web config e.g.
65: $webVersion = get_version
66: Write-Host "WebsiteVersion: $webVersion"
67:
68: # 2. Create new version folder if not exists69: $deployPath = create_new_version_folders
70:
71: # 3. Copy package
72: copy_package $deployPath
73:
74: # finally reset path75: pop-location
76: exit
In the first couple of lines I’m declaring where to find the packages and a list of the configurations that target each of the different deployment environments. In line 9 is my function to retrieve the current version number from Web.config, again using the PowerShell [xml] facility. create_new_version_folders in line 22 does what its name suggests and creates the file structure where the installers will end up. And finally copy_package copies each installer to the correct location in the new directory structure.
For other projects I modify this script to copy e.g SQL scripts used to update the database for a particular version of the application. This can include modifying the SQL to match up any linked servers used in a particular deployment environment.
The amount of time that saves me is significant and makes my day just a little bit better each time. The converse is also true, whenever I find myself having to do something manually more than once I curse myself for not having spent the time to automate it the first time.