There are multiple ways you can bring code and content into your Sitecore instance. Here at BrainJocks we are big fans of TDS, Git, cloud infrastructures, and Atlassian toolset – our local deployments are TDS-powered, our continuous build and deployment vehicle is Elastic Bamboo, and with little PowerShell, curl, and Sitecore.Ship we push code and content into integration and QA environments in EC2 and Azure.
.update
TDS build produces a Sitecore .update package as a deployment artifact. It’s a zip over a package.zip with the contents that has a certain structure that Sitecore can understand and process. In short, it has items to be installed, files to be deployed, and metadata describing the package as well as the content it brings (e.g. package description, collision resolution strategy for each file, etc.). An update package can also tell Sitecore to run a custom post installation step.
.config
A .config file is, for lack of a better word, a config file. We all customize and configure Sitecore with .config files in App_ConfigInclude.
.update + .config
Sitecore installer is not an equal opportunity employer. At least two categories of citizens get special treatment – __Standard Values and .config files. I will leave __Standard Values for another blog post, it’s an interesting topic but it doesn’t play any role in the love story of the .update and .config.
Sitecore is very gentle with the .config files, as you have probably experienced if you used .update packages. Specified collision strategy notwisthstanding, Sitecore won’t overwrite the one it has with the one supplied by the .update package. That’s exactly what you want it to do but it just won’t. Here’s the hard evidence from Sitecore.Update.Installer.Items.AddFileCommandInstaller (simplified for illustrative purposes):
protected override void DoInstall( ... ) { if (fileName.EndsWith(".config") { context.WriteCommandProcessingMessage("Preparing to install file"); HandleConfigurationFileAlreadyExists( ... ); } else { context.WriteCommandProcessingMessage("Installing file"); } } protected void HandleConfigurationFileAlreadyExists( ... ) { if (FilesAreTheSame) { return; // Skip } fileName = fileName + "." + context.PackageName; return; // Force the file in with the new name }Love?
When you bring an updated
.configfile via the.updatepackage there’s no love. You end up with the updated.configfile saved with the package name added to its name alongside the original file. Your changes are not active until you manually activate them.How can we help the two be together?
Sitecore
It would be nice if we could optionally instruct Sitecore in the
.updatepackage metadata to go ahead and force the.configfile in. The installation process it not exposed via a pipeline so we can’t customize it without a help from the product team. A feature request worth submitting but not something we can expect overnight.TDS
TDS runs a post installation step. We could piggyback on it if only customizing what happens post install was exposed as a project property. It’s not. We can, of course, unzip the
.updateand then unzip thepackage.zipto tap into the metadata but then there’s another hurdle. There’s only one post installation step per package (another feature request to Sitecore?). We can’t piggyback per se, we would need to wrap around and substitute. Possible (and I have done it with PowerShell) but it’s a) tedious, and b) what if Hedgehog guys decide to change the way they run the recursive deploy in the next version? Probably a feature request worth submitting to Hedgehog and maybe a faster turnaround but still not something we can expect overnight.Sitecore.Ship
Can Sitecore.Ship do it? Mike Edwards recently submitted a patch to run the post install step that TDS needs. I did a few small updates on top of it and could probably also include the commit configuration step. A patch worth submitting but let’s first do something right here. Let’s help
.updateand.confighave their happy-ever-after right now.Love!
We need two things – a controller to expose an endpoint and a
ConfigFileCommitterservice to do the work.Controller
An non-rendering MVC controller needs a route that you register via a pipeline. I suggest:
/outsmartsitecore/configuration/commit/{id}where
configurationstands forConfigurationController, its only action iscommit, andidis for the package name. The controller doesn’t do much:[HttpPost] public ActionResult Commit(string id) { if (string.IsNullOrEmpty(id)) { Response.StatusCode = (int) HttpStatusCode.InternalServerError; return Json(new {error = "Package name cannot be empty"}); } var committer = new ConfigFilesCommitter(id); var path = Sitecore.IO.FileUtil.MapPath("/App_Config/Include"); Dictionary<string, string> result = committer.Process(path); return Json(result); }It requires that a packge name be provided (to make it very targeted and a little more secure) and it only runs for the
App_ConfigIncludepath. TheConfigFileCommitterdoes the rest.Config Files Committer
/// <summary> /// Renames *.config files after a Sitecore .update package install /// by removing the package name from their names. /// </summary> public class ConfigFilesCommitter { private readonly string _pattern; private readonly Regex _renamer; /// <summary> /// Creates new instance and initializes it to match config files with a given package name. /// </summary> /// <param name="packageName">Installed package name or -GUID- for a wildcard match</param> public ConfigFilesCommitter(string packageName) { Assert.ArgumentNotNullOrEmpty(packageName, "packageName"); // Sitecore.Ship creates temp files (GUID as their name) for uploaded packages if (string.Equals("-GUID-", packageName, StringComparison.OrdinalIgnoreCase)) { // it's ok to have the file listing pattern "open", the regex is strict and // the replacer will skip files that were not renamed _pattern = @"*.config.*"; _renamer = new Regex(@".config.[wd]{8}-[wd]{4}-[wd]{4}-[wd]{4}-[wd]{12}"); } else { _pattern = string.Format(@"*.config.{0}", packageName); _renamer = new Regex(_pattern.Replace("*", "").Replace(".", "\.")); } } /// <summary> /// Lists files to be renamed by listing everything in the given directory /// that matches this committer's pattern. /// </summary> /// <param name="path">Path where to look for files to be renamed</param> /// <returns>List of files to be renamed</returns> public IEnumerable<FileInfo> ListFilesToCommit(string path) { if (!Directory.Exists(path)) { Log.Warn(string.Format("Cannot commit config files. Path {0} does not exists", path), GetType()); return Enumerable.Empty<FileInfo>(); } return (new DirectoryInfo(path)).EnumerateFiles(_pattern); } /// <summary> /// Renames a file with a package name postfix back to its intended .config name /// </summary> /// <param name="file">Original file name</param> /// <returns>New file name after replacement</returns> public string Rename(string file) { return _renamer.IsMatch(file) ? _renamer.Replace(file, ".config") : file; } /// <summary> /// Commits .config files by removing package name postfix from their name. /// </summary> /// <param name="path">Path where to rename files</param> /// <returns>Renamed files report (original file names as keys and new names as values)</returns> public Dictionary<string, string> Process(string path) { var result = new Dictionary<string, string>(); foreach (FileInfo file in ListFilesToCommit(path)) { string newName = Rename(file.FullName); // safeguard not to do unnecessary file operations when using GUID wildcard if (string.Equals(file.FullName, newName, StringComparison.OrdinalIgnoreCase)) { continue; } if (File.Exists(newName)) { File.Delete(newName); } File.Move(file.FullName, newName); result.Add(file.FullName, newName); } return result; } }And, of course, the unit tests (not published here for brevity)
Happy End
The Bamboo now needs to run one more
curlcommand right after Sitecore.Ship:$ curl -F "id=-GUID-" http://<url>/outsmartsitecore/configuration/commitThe continuous deployment with TDS and Sitecore.Ship is now truly unattended and
.configand.updateare finally together. I hope they lived happily ever after.

Pavel – great article!! One thought I had, it might be a little easier to add a simple .aspx file to the sitecore/admin folder that will kick off the committer action – it’s also easy to add some basic http authentication, etc.
Mike’s post install step feature has been merged into Sitecore.Ship now.
True. We were using it from the fork all along to make sure Hedgehog’s recursive deploy runs. That said, the latest TDS (5.1) does not yet expose a customization point to tap into the post deploy step with something like configuration commit. And
.updatepackage metadata only supports a single post-deploy action. I actually scripted unpacking the.updateto inject my own class wrapped around Hedgehog’s to do my thing and their thing together but it’s too much mockery and not very future-proof. Dropped that idea in favor of a simple controller endpoint that I cancurlinto right after deploy. I heard the Hedgehog guys say a few times that 5.2 would enable customizing the post deploy action. Can’t wait! Though I know they are busy getting ready for VS 2015.