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
.config
file via the.update
package there’s no love. You end up with the updated.config
file 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
.update
package metadata to go ahead and force the.config
file 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
.update
and then unzip thepackage.zip
to 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
.update
and.config
have their happy-ever-after right now.Love!
We need two things – a controller to expose an endpoint and a
ConfigFileCommitter
service 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
configuration
stands forConfigurationController
, its only action iscommit
, andid
is 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_ConfigInclude
path. TheConfigFileCommitter
does 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
curl
command 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
.config
and.update
are 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
.update
package metadata only supports a single post-deploy action. I actually scripted unpacking the.update
to 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 cancurl
into 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.