Posts Tagged ‘ Continuous Integration ’

ClickOnce Master Build

Previously I have posted about how to publish a ClickOnce release for multiple environments. Whilst this works well there are two reasons (that I can think of) why it is not appropriate or good enough.

First, you or your manager may be a bit of a purist in terms of releasing to Production ‘exactly’ what has been tested, specifically the exact same compiled assemblies.

Second, you may need to create a release for several different customers who each have several of their own ‘environments’ but you do not want to provide them with your source code.

Third…both of the above. 🙂

When you ‘Publish’ with MsBuild it requires the source code and it recompiles your assemblies. So how can we easily create releases for multiple environments but only Publish once? This posting offers one solution which has worked for me in the past, it’s not a complete ‘how to’ guide I just wanted to cover the idea and problems I ran into along the way.

Master Build Environment

Make sure you take a look at my previous posting as this process builds on those ideas.

One of the final steps of this process is to re-sign the setup.exe, however in order for that final step to work you have to change one of your ClickOnce deployment settings in your .csproj file.

<SignManifests>false</SignManifests>

SignManifests must be false! Otherwise the signtool gets confused when re-signing the setup.exe. Check this out for a bit more info on this known issue.

So the first thing to do is create a BuildEnvironment called Master. The configuration details of this (web service addresses or database connnection strings etc) should point to nowhere, i.e. if someone deployed this build then it should not work. Publishing this is easy just follow the steps defined in the multiple environment posting, the process of creating a release with environment specific information that is tricky.

MsBuild Changes for Master Build Publish

When you publish with ClickOnce it creates a folder ‘Application Files’ where it puts the contents of the release. The space in that file name causes a problem with MAGE when re-signing the app. Now the only way I could see to change this was to cut and paste the _CopyFilesToPublishFolder target from Microsoft.Common.Targets and modifying the _DeploymentApplicationFolderName property in my version to remove the space. I think it is a really BAD idea to change the Microsoft.Common.Targets directly so that option was ruled out immediately and as you can see in the code below, Application Files (version below already has the space removed) is a hardcoded value, not a settable property…therefore even though it is ugly, cut and paste seems the only viable solution.

      <!--
	  This Target code has been cut and paste from Microsoft.Common.Targets.
	  Only ONE thing has been changed.
	  Application Files changed to ApplicationFiles (i.e. the space removed)
	  The only other way of changing that value is to modify Microsoft.Common.Targets
	  which is a really bad idea (would have to ensure this was changed on ALL machines.)

	  The space causes a problem when re-signing the app with MAGE
    ============================================================
                                        _CopyFilesToPublishFolder
    ============================================================
    -->
    <Target
        Name="_CopyFilesToPublishFolder">

        <!-- Compute name of application folder, which includes the assembly name plus formatted application version.
             The application version is formatted to use "_" in place of "." chars (i.e. "1_0_0_0" instead of "1.0.0.0").
             This is done because some servers misinterpret "." as a file extension. -->
        <FormatVersion Version="$(ApplicationVersion)" Revision="$(ApplicationRevision)" FormatType="Path">
            <Output TaskParameter="OutputVersion" PropertyName="_DeploymentApplicationVersionFragment"/>
        </FormatVersion>

        <PropertyGroup>
            <_DeploymentApplicationFolderName>ApplicationFiles\$(AssemblyName)_$(_DeploymentApplicationVersionFragment)</_DeploymentApplicationFolderName>
            <_DeploymentApplicationDir>$(PublishDir)$(_DeploymentApplicationFolderName)\</_DeploymentApplicationDir>
        </PropertyGroup>

        <!-- Copy files to publish folder -->
        <Copy
            SourceFiles=
                "@(_ApplicationManifestFinal);
                @(_DeploymentResolvedManifestEntryPoint);
                @(_DeploymentManifestFiles);
                @(ReferenceComWrappersToCopyLocal);
                @(ResolvedIsolatedComModules);
                @(_DeploymentLooseManifestFile)"
            DestinationFiles=
                "@(_ApplicationManifestFinal->'$(_DeploymentApplicationDir)%(TargetPath)');
                @(_DeploymentManifestEntryPoint->'$(_DeploymentApplicationDir)%(TargetPath)$(_DeploymentFileMappingExtension)');
                @(_DeploymentManifestFiles->'$(_DeploymentApplicationDir)%(TargetPath)$(_DeploymentFileMappingExtension)');
                @(ReferenceComWrappersToCopyLocal->'$(_DeploymentApplicationDir)%(FileName)%(Extension)$(_DeploymentFileMappingExtension)');
                @(ResolvedIsolatedComModules->'$(_DeploymentApplicationDir)%(FileName)%(Extension)$(_DeploymentFileMappingExtension)');
                @(_DeploymentLooseManifestFile->'$(_DeploymentApplicationDir)%(FileName)%(Extension)$(_DeploymentFileMappingExtension)')"
            SkipUnchangedFiles="true"
            OverwriteReadOnlyFiles="$(OverwriteReadOnlyFiles)"/>
        <Copy
            SourceFiles="@(_DeploymentManifestDependencies)"
            DestinationFiles="@(_DeploymentManifestDependencies->'$(_DeploymentApplicationDir)%(TargetPath)$(_DeploymentFileMappingExtension)')"
            SkipUnchangedFiles="true"
            OverwriteReadOnlyFiles="$(OverwriteReadOnlyFiles)"
            Condition="'%(_DeploymentManifestDependencies.DependencyType)'=='Install'"/>
        <Copy
            SourceFiles="@(_ReferenceScatterPaths)"
            DestinationFiles="@(_ReferenceScatterPaths->'$(_DeploymentApplicationDir)%(Filename)%(Extension)$(_DeploymentFileMappingExtension)')"
            SkipUnchangedFiles="true"
            OverwriteReadOnlyFiles="$(OverwriteReadOnlyFiles)"/>
        <FormatUrl InputUrl="$(_DeploymentApplicationUrl)">
            <Output TaskParameter="OutputUrl" PropertyName="_DeploymentFormattedApplicationUrl"/>
        </FormatUrl>
        <FormatUrl InputUrl="$(_DeploymentComponentsUrl)">
            <Output TaskParameter="OutputUrl" PropertyName="_DeploymentFormattedComponentsUrl"/>
        </FormatUrl>
    </Target>

Note that the space only causes a problem with MAGE.exe, it does not cause a problem with MAGEUI.exe! MAGEUI.exe happily re-signs the release even with the space in the directory name. We cannot use MAGEUI.exe however because this process needs to be automated, if you use MAGEUI.exe someone has to run it and manually set the values…not good enough.

It took a while to figure out that the space in the dir name was causing a problem so I hope this saves someone else some time…

Creating a Release

Ok so now we have a published build that no one can use, the first place we want to release to will be QA, then from there UAT so what we need now is an automated way of creating a release configured for these environments.

Creating a release should be as easy as publishing a build, it should be a one step process.

Create a batch file ‘createRelease.bat’ which performs all of the necessary steps. Those steps are:

  1. Create or clean the ‘working’ directory.
  2. Copy the master build into the working directory.
  3. Run the MsBuild steps.
  4. Create a zip file of the new release. (not necessary just makes it a little easier to move the releases around)

Run the MsBuild Steps

Add a new target in your customized.targets file, ModifyMasterBuildForEnvironment.

<Target Name="ModifyMasterBuildForEnvironment" DependsOnTargets="SetPropertyValues; ConfigureForEnvironment; RecreateManifests" />

SetPropertyValues

There are a few properties which need to be set for the resigning process.

	<Target Name="SetPropertyValues" >
		<Message Text="FullReleasePath $(FullReleasePath)" />

		<MSBuild.ExtensionPack.Framework.TextString TaskAction="Replace" OldString="$(ApplicationVersion)" OldValue="." NewValue="_">
			<Output PropertyName="buildX" TaskParameter="NewString"/>
		</MSBuild.ExtensionPack.Framework.TextString>

		<PropertyGroup>
			<WorkingDirectory>WorkingDirectory</WorkingDirectory>
			<FullReleasePath>$(WorkingDirectory)\ApplicationFiles\$(AssemblyName)_$(buildX)</FullReleasePath>
			<ApplicationManifestName>$(AssemblyName).exe.manifest</ApplicationManifestName>
			<ApplicationManifestPath>$(FullReleasePath)\$(ApplicationManifestName)</ApplicationManifestPath>
			<DeploymentManifestPath>$(WorkingDirectory)\$(DeploymentManifestName)</DeploymentManifestPath>
			<CertFileName>cert\pmsKey.pfx</CertFileName>
			<ProviderFullUrl>$(ProviderBaseUrl)$(DeploymentManifestName)</ProviderFullUrl>
		</PropertyGroup>
	</Target>

ConfigureForEnvironment

Add your own target(s) here that does the work to configure the release for a given environment. e.g. update the config file setting database connection strings or web service urls.

RecreateManifests

Now that you have modified the published release it is no longer a valid ClickOnce release. Try to install it and it will fail. What you need to do is update and re-sign the manifests (the Application Manifest and the Deployment Manifest). You also need to update and resign the Setup.exe!

	<Target Name="RecreateManifests">

		<!-- Application manifest update and resign -->
		<Message Text="Removing .deploy suffix..." />
		<RenameFiles DirectoryPath="$(MSBuildProjectDirectory)\$(FullReleasePath)" RemoveSuffix=".deploy" IncludeSubDirectories="True" />
		<Message Text="Updating application manifest" />
		<Exec Command="mage -Update $(ApplicationManifestPath) -FromDirectory $(FullReleasePath) -ToFile $(ApplicationManifestPath)" />
		<Message Text="Adding .deploy suffix..." />
		<RenameFiles DirectoryPath="$(MSBuildProjectDirectory)\$(FullReleasePath)" AppendSuffix=".deploy" IncludeSubDirectories="True" ExcludedFiles="$(ApplicationManifestName)" />
		<Message Text="ApplicationManifestPath $(ApplicationManifestPath)" />
		<Exec Command="mage -Sign $(ApplicationManifestPath) -CertFile $(CertFileName)" />

		<!-- Deployment manifest update and resign -->
		<Message Text="Updating Deployment manifest (ProviderFullUrl=$(ProviderFullUrl))" />
		<Exec Command="mage -Update $(DeploymentManifestPath) -AppManifest $(ApplicationManifestPath) -ProviderUrl $(ProviderFullUrl)" />
		<Message Text="DeploymentManifestPath $(DeploymentManifestPath)" />
		<Exec Command="mage -Sign $(DeploymentManifestPath) -CertFile $(CertFileName) " />

		<!-- Setup.exe update and resign -->
		<Message Text="Updating $(WorkingDirectory)\Setup.exe..." />
		<Exec Command="$(WorkingDirectory)\Setup.exe /url=$(ProviderBaseUrl)" />
		<Message Text="Signing $(WorkingDirectory)\Setup.exe..." />
		<Exec Command="signtool sign /v /f $(CertFileName) $(WorkingDirectory)\Setup.exe" />

	</Target>

Note that before Updating the Application Manifest we have to remove the .deploy extension from the file names, then add the .deploy extension back onto the file names before Signing. (if you have configured ClickOnce to NOT append .deploy then obviously just remove these steps).

RenameFiles Task

To get the renaming behaviour I wanted I had to create my own MsBuild task. You will need to do the same, it’s a little crude as I had limited time but this is the simple little task I created….

    public class RenameFiles : Task
    {
        public override bool Execute()
        {
            var directory = new DirectoryInfo(DirectoryPath);
            if(directory == null)
                throw new ApplicationException(string.Format("Directory path is invalid: {0}", DirectoryPath));
            Console.WriteLine(string.Format("DirectoryPath:{0}", DirectoryPath));

            ProcessDirectory(directory);

            return true;
        }

        private void ProcessDirectory(DirectoryInfo directory)
        {
            RenameAllFiles(directory);
            if(! IncludeSubDirectories)
                return;

            var subs = directory.GetDirectories();
            foreach (DirectoryInfo sub in subs)
                ProcessDirectory(sub);
        }

        private void RenameAllFiles(DirectoryInfo directory)
        {
            var files = directory.GetFiles();

            if(files.Length == 0)
                return;

            foreach (FileInfo file in files)
            {
                string fileName = file.Name;
                if (ExcludedFileList.Exists(name => name.Equals(fileName, StringComparison.InvariantCultureIgnoreCase)))
                {
                    Console.WriteLine(string.Format("{0} is excluded from renaming.", file.Name));
                    continue;
                }
                Console.WriteLine(string.Format("Renaming {0}", file.Name));
                RemoveExtensions(file);
                AppendExtensions(file);
            }
        }

        private void RemoveExtensions(FileInfo file)
        {
            if (string.IsNullOrEmpty(RemoveSuffix))
                return;
            file.MoveTo(file.FullName.Replace(RemoveSuffix, string.Empty));
        }

        private void AppendExtensions(FileInfo file)
        {
            if (string.IsNullOrEmpty(AppendSuffix))
                return;
            file.MoveTo(string.Format("{0}{1}", file.FullName, AppendSuffix));
        }

        public string DirectoryPath { get; set; }
        public string RemoveSuffix { get; set; }
        public string AppendSuffix { get; set; }
        public string ExcludedFiles { get; set; }
        public bool IncludeSubDirectories { get; set; }

        private List<string> ExcludedFileList
        {
            get
            {
                if (excludedFileList == null)
                    excludedFileList = CommonFunctions.CreateListFromCommaSeparatedString(ExcludedFiles);
                return excludedFileList;
            }
        }
        private List<string> excludedFileList;
    }

Summary

There are a number of other little steps we do to make this release process work here but these are really specific to how we want it to work. What I have outlined above is the core steps involved including some problems you will face and their solutions. I hope this posting helps you get over the most challenging of the hurdles you will face in setting a smooth ‘Master Build’ release process but there is still alot of extra steps you will need to create yourself to get it working end to end.

One thing I would recommend is, once you have a working createRelease.bat (note it doesn’t have to be a batch file, could be an exe or powershell or whatever works best in your situation) then create a createReleaseALL.bat file. In there make n calls to createRelease.bat…one for each environment you need to create releases for. That way if you have a dozen environments you can still create a release for all of them with a one step process 🙂

I have had to do this fairly quickly so if something is not clear please let me know and I will see if I can improve the post over time.

Final note – It does take time to set this up and get it working correctly, but it is so worth it! Over time you will save much more time than you spent setting it up, guaranteed!

Additional Resources

A few links which helped me to piece this puzzle together…

ClickOnce with bootstraper setup.exe need to change URL without any build

packaging-a-clickonce-server-deployment

Getting Started With Continuous Integration

In Less than Ten Minutes!

This article is all about how you can easily get up and running with CI in less than 10 minutes!

There are really two separate parts to gettings started with CI (and therefore two parts to this article).

  1. Setting up the build server and
  2. Creating the build (e.g. Nant files)

Then there are the issues you will face and need to find some form of resolution for.

First you need to download and install these tools:-

  1. Cruise Control
  2. CCTray (same location as CruiseControl)
  3. NAnt
  4. NAntGenie

And Add the path to these exe files in your Environmental Variables

  1. NAnt
  2. msbuild (something like ‘C:\Windows\Microsoft.NET\Framework\v3.5’)
add path to NAnt and msbuild

add path to NAnt and msbuild

Presumed Knowledge

These are demo notes from a ‘lightning talk’ I gave at the Sydney Alt.Net user group in February.

In the talk I had only 10 minutes, so I made the presumption that all in attendance knew what CI was, if you don’t then check out these links, they will get you started:-

Martin Fowler

Extreme Programming Rules

The Build Server

I’ve always used Cruise Control as it has always met my needs, is free, simple to get up and runnning and pretty much trouble free. For the demo I used version 1.4.2, when you download it, make sure you also get CCTray.

There are a number of other build servers out there that are definately worth looking at and comparing, I’m especially keen to try out Jetbrain TeamCity…when I find some time. It doesn’t matter which one you choose.

Setup CCTray

  1. Install Cruise Control (keep all of the default settings)
  2. Install CCTray (start it)
  3. Ok so now open CCTray settings and add (try to) a new build server.

ci_cctraysetupbuildserver2

Connect directly using .Net Remoting is the default, change it to Via the CruiseControl.NET dashboard (just a recommendation).

Click OK and you should see at the bottom a message saying, ‘Failed to connnect to server….’. We going to fix that right now.

Configure The Server

First thing we need to do is configure the service.

  1. Go here [C:\Program Files\CruiseControl.NET\server] or wherever you chose to install it.
  2. Open ccnet.config
  3. This is where things get personal. The example below is a basic configuration to point to an SVN Repository. The documentation on the Cruise Control website is concise and easy to follow so if your needs are different you’ll have no problems figuring out what you need to do.
  4. Add a ‘Project’ with the follow config sections:
    1. Trigger
    2. Source Control
    3. Labeller
    4. Build Task
    5. Publishers
  5. Save and double click on ccnet.exe
  6. You now have a working automated build! (well sort of…we still need to do the second part of the process, THE BUILD!)


TIP: Don’t forget to change the verbosity of the output to INFO (it’s currently DEBUG). Don’t worry CC will remind you every time it starts and tell you where the config file is that you need to change.

This is the contents of the config used in the demo, change it to suit your environment and save. Make sure:-

  • The path to NAnt.exe is correct.
  • The trunkURL for your repository is correct (or replace with appropriate section for your repository)
  • Working directory and publish from directories are the same.
  • You may need to make sure the SuccessfulBuilds directory exists before running…

Create the Build Files

The other big piece of the puzzle is the master.build file and the associated myProject.build files.

My tool of choice here is NAnt and I use (wrote) this generator to automatically create my build files and the master.build.

To create these use NantGenie (be sure to read the Notes section)…or you can write them from scratch.

RUN

That’s it you should be good to go. Start ccnet, checkin a change to your source, the build should start and run your new master.build file!

White Lie

Ok so here’s where I have to come clean. It may have been a little white lie when I said that you could get CI up and running in less than 10 minutes. You are going to run into some other issues that is going to make it take….longer.

I discuss some of these issues in this article. But in a nutshell these will be the biggest thorns in your side:-

1. Database changes

Ideally when you check your code in, you are checking in a ‘unit of work’. This could include some app code changes AND some database changes, e.g. a new column and some data. Your tests rely on the database being up to date, so how do you ensure that the database on the build server DOES get updated when you checkin?

I’ve created an automated Db updater and put it on codeplex, it works for me. Check it out and I recommend looking at what other alternatives might work in your unique environment.

2. App.Config changes

So what is the best way to ‘manage’ the app.config files?

I don’t have a definitive answer for this one but I think that the following conditions are ‘desirable’ when planning how to manage your app.config.

  1. Like other code, you don’t want to have duplicates (including cross project).
  2. You only want data in there that is relevant to the project.
  3. Must be under source control.
  4. You want to be able to define what the variable contents should be in one place only. (where doesn’t matter too much as long as it is only one place)
  5. When you make changes to your App.Config, you want to make them whilst you are working on your relevant code change and then forget about it once it has been checked in. By ‘forget about it’ I mean you don’t have to remember to update the file on the UAT box when promoting a new build there and then remember to do the same change to Production…

All of this is tricky with an automated build and multiple environments.

3. People

Probably your biggest hurdle will be convincing people that CI helps and that they need to CHANGE their habits a little (sometimes alot) in order to make it work.

Tips

  • If you’re struggling to figure out why the damn build is broken, the best place to look is here [C:\Program Files\CruiseControl.NET\server\YourProject\Artifacts\log].
  • Also, don’t forget you can also turn the verbosity of the CC server back to DEBUG.

Conclusion

Hopefully this article will help reduce the time it takes to get some CI action happening in your team. Yes it is going to take more than 10 minutes but I guarantee you it is worth the effort and once you start using an automated build you simply refuse to go back to the chaos that preceded it!

Secrets for Setting up Continuous Integration

I’ve set up CI in my last two roles and have spent a little bit of time trying to simplify the process whilst doing it and thought I’d share a few ‘secrets of sucess’ for getting CI up and running.

So what makes CI easier?

1. GENERATE YOUR BUILD FILES

I wrote my own code generator that searches through my entire dev directory for project files (I work with .Net so it simply looks for .csproj files) and generates a NAnt build file for each project file. I also generate the master.build file. If there isn’t already an existing CodeSmith template for this type of thing then it’s not too hard to create your own generator (code generator = fancy string builder). This is what I’ve done.

Generating your build files is also important because you then know that all of your projects are built in a consistent manner. Plus when you make the build files cleverer you can easily apply those changes across all of your build files at the push of a button (ok a few buttons maybe).

2. AUTOMATE AND CONTROL YOUR DATABASE UPDATE PROCESS

This is a whole other topic in itself but I’ll try to keep it brief (of course ignore this one if your app does not hit a database). The biggest challenge you will face is handling database updates effectively. Your build box, test box and of course production box all have their own database and ideally each developer runs their own local copy (unless size does not permit). The problem is, when a developer checks in his unit of work which includes a database change (new table and some data for example), when the build kicks off how do you ensure that those database changes are run against the build boxes database?

You must have an automated program that runs any new scripts and this program must be one of the first things run as part of your build process. So what if one of the scripts fails? Ideally your program will run all scripts in a transaction and simply rollback the transaction if any of the scripts fail. This will work on SQL Server but sadly on Oracle any DML script will cause a commit of the transaction so you must find another (like backup first and restore on failure).

Developers use the same program to update their local database and the program is also used when promoting to other environments. By the time you run it against production you will potentially have hundreds of scripts (which I have experiences) so make sure you have a good naming convention (or other technique) that ensures the scripts get run in the correct order.

All changes to the database must be made with a script, no exceptions. (not exactly a new rule but it amazes me how many people do not follow it!)

I wrote my own program to handle this and I’d be very interested to hear what others have done.

3. GET ‘BUY IN’ BEFORE STARTING

From developers to the CIO. The CIO needs to understand the benefits that this new process will provide because it is going to take time (and money) to get it setup (money and time that will DEFINATELY be recouped over time).

CI forces a more disciplined approach to software development, some developers will not like that so it’s important that they understand the benefits that they will see with the new process (like knowing that doing a get latest will always return a working copy of the source!). Developers will get annoyed with the new process if they don’t understand it properly and keep breaking the build.

4. GET A DECENT BUILD BOX

We are still working with a crappy old desktop. It takes longer to build and we continually have clean it up as it fills up pretty quickly. Frustrating!

5. MAKE RULES FROM DAY 1

#1 – If you break the build it becomes your highest priority to fix it.
#2 – If the build is broken no further checkins are allowed until it is fixed. (except of course by the guy fixing)
#3 – No chicken runs…means no checking in just before you go home. (this rule is the only one that is flexible)

6. CLEAN BUILD

If you delete everything in your development directory on you build box (i.e. where all of the source code is) and then start the build it….it should work! If it doesn’t then there is a dependency in there somewhere that is not in source control.

Every developer I have spoken to who has used CI tells me there is no way they could work in an environment that does not use it, so it may present some challenges setting it up but it is well worth it!

Update…Check out this article too!