TDS Post Deploy Action : Publish Items Via Publishing Queue

October 10, 2017

Blog | Development | TDS Post Deploy Action : Publish Items Via Publishing Queue
TDS Post Deploy Action : Publish Items Via Publishing Queue

As you may or may not know, Team Development for Sitecore (TDS) Classic can run Post Deploy Actions after a package is installed. Out-of-the-box, the available actions are: Trigger Save Event, Publish After Deploy and Update Link Database. Aside from these, the community has provided some excellent custom actions that can be easily added to projects. Recently, I added the built-in Publish After Deploy action to a project.

I was surprised after viewing the logs that TDS was issuing a SingleItem publish for each item that was deployed in the package. Each publish in Sitecore executes the publish:end  and publish:end:remote events after a successful publish. This meant after each SingleItem publish, every site cache was cleared and more importantly, the indexes ran a single update operation – among several other native events. With potentially hundreds of updated items, this was an unnecessary number of operations that were filling up the event queue and ultimately slowing down our releases. I solved this issue with a modified Publish Deployed Items With Publishing Queue action.

Publish Deployed Items With Publishing Queue

The code below borrows from the existing Publish After Deploy action and tweaks it by adding each deployed item first to the Publishing Queue, and then publishing the entire queue at once. This reduces the publishing operations to 1, ultimately running the publish:end and publish:end:remote events once each. The code is also available at this public gist.

[Description("Publishes deployed items after deployment using publishing queue.\nPublishing targets are specified in the Parameter as a comma separated list.")]
public class PublishDeployedItemsWithPublishingQueue : IPostDeployAction
{
    /// <summary>
    /// Borrowed directly from HedgehogDevelopment.SitecoreProject.PackageInstallPostProcessor.BuiltIn.PublishDeployedItems, HedgehogDevelopment.SitecoreProject.PackageInstallPostProcessor
    /// </summary>
    private static Database[] GetPublishingTargetDatabases(string parameters)
    {
        if (string.IsNullOrEmpty(parameters))
        {
            throw new InvalidOperationException("Please specify a publishing target in the parameter");
        }
        var source = (from t in parameters.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
                        join p in PublishManager.GetPublishingTargets(Database.GetDatabase("master")) on t.Trim().ToLower() equals p.Name.ToLower()
                        select Database.GetDatabase(p["Target Database"]) into d
                        where d != null
                        select d).Distinct().ToArray();
        if (!source.Any())
        {
            throw new InvalidOperationException("Could not locate publishing target databases");
        }
        return source;
    }

    /// <summary>
    /// Borrowed directly from HedgehogDevelopment.SitecoreProject.PackageInstallPostProcessor.BuiltIn.PublishDeployedItems, HedgehogDevelopment.SitecoreProject.PackageInstallPostProcessor
    /// </summary>
    private static Language[] GetPublishingTargetLanguages(Database[] publishingTargetDatabases)
    {
        if (publishingTargetDatabases == null)
            throw new ArgumentNullException(nameof(publishingTargetDatabases));

        var source = new List<Language>();
        foreach (var database in publishingTargetDatabases)
        {
            source.AddRange(LanguageManager.GetLanguages(database));
        }
        return source.Distinct().ToArray();
    }

    public void RunPostDeployAction(XDocument deployedItems, IPostDeployActionHost host, string parameter)
    {
        Log.Info("Running TDS Post Deploy Action :: PublishDeployedItemsWithQueue", this);

        var publishingTargetDatabases = GetPublishingTargetDatabases(parameter);
        var publishingTargetLanguages = GetPublishingTargetLanguages(publishingTargetDatabases);

        var publishingCandidates = new List<PublishingCandidate>();

        var sourceDb = Database.GetDatabase("master");

        PostDeployActionSupport.ExecuteOnAllDeployedItems(deployedItems, delegate (XElement deployedItemInPackage, Guid deployedItemId, string databaseName)
        {
            if (databaseName == "core")
                return;

            if (databaseName != "master")
                Log.Info(":: PublishDeployedItemsWithQueue, unknown database -> " + databaseName, this);

            publishingCandidates.AddRange(
                PublishUtils.CreatePublishingCandidatesFromItemId(sourceDb, publishingTargetDatabases, publishingTargetLanguages, new ID(deployedItemId))
            );
        });

        Log.Info(":: PublishDeployedItemsWithQueue, targetDatabases", this);

        foreach (var db in publishingTargetDatabases)
        {
            Log.Info(":: PublishDeployedItemsWithQueue, targetDatabase -> " + db.Name, this);
        }

        Log.Info(":: PublishDeployedItemsWithQueue, targetLanguages", this);

        foreach (var lang in publishingTargetLanguages)
        {
            Log.Info(":: PublishDeployedItemsWithQueue, targetLanguage -> " + lang.Name, this);
        }

        Log.Info(":: PublishDeployedItemsWithQueue, number of publishing candidates -> " + publishingCandidates.Count, this);

        PublishUtils.ProcessCandidates(sourceDb, publishingTargetDatabases, publishingTargetLanguages, publishingCandidates);
    }
}

The code here relies on this public Gist. It is a small library of extensions to easily add groups of items to the publishing queue and execute the publishing operation once. I’ve included the code below as well:

public static class PublishUtils
{
    public static void CreateAndPublishQueue(Database sourceDatabase, Database[] targetDatabases, Language[] targetLanguages, IEnumerable<ID> itemIds, bool skipEvents = false, bool useSecurityDisabler = true)
    {
        Assert.IsNotNull(sourceDatabase, "sourceDatabase");
        Assert.IsNotNull(targetDatabases, "targetDatabases");
        Assert.IsNotNull(targetLanguages, "targetLanguages");

        var publishingCandidates = SetPublishingCandidates(sourceDatabase, targetDatabases, targetLanguages, itemIds);

        ProcessCandidates(sourceDatabase, targetDatabases, targetLanguages, publishingCandidates, skipEvents, useSecurityDisabler);
    }

    private static List<PublishingCandidate> SetPublishingCandidates(Database sourceDatabase, Database[] targetDatabases, Language[] targetLanguages, IEnumerable<ID> itemIds)
    {
        var publishingCandidates = new List<PublishingCandidate>();

        foreach (var itemId in itemIds)
        {
            publishingCandidates.AddRange(CreatePublishingCandidatesFromItemId(sourceDatabase, targetDatabases, targetLanguages, itemId));
        }

        return publishingCandidates;
    }

    public static List<PublishingCandidate> CreatePublishingCandidatesFromItemId(Database sourceDatabase, Database[] targetDatabases, Language[] targetLanguages, ID itemId)
    {
        if (sourceDatabase == null)
        {
            Log.Info("sourceDatabase == null", new object());
            return Enumerable.Empty<PublishingCandidate>().ToList();
        }
        if (targetDatabases == null)
        {
            Log.Info("targetDatabases == null", new object());
            return Enumerable.Empty<PublishingCandidate>().ToList();
        }
        if (targetLanguages == null)
        {
            Log.Info("targetLanguages == null", new object());
            return Enumerable.Empty<PublishingCandidate>().ToList();
        }

        var publishingCandidates = new List<PublishingCandidate>();

        var item = sourceDatabase.GetItem(itemId);

        if (item == null)
        {
            Log.Info("item == null", new object());
            return Enumerable.Empty<PublishingCandidate>().ToList();
        }

        foreach (var publishingTargetDatabase in targetDatabases.Where(tDb => tDb != null))
        {
            foreach (var publishingTargetLanguage in targetLanguages.Where(lang => lang != null))
            {
                var publishOptions = new PublishOptions(
                    Database.GetDatabase("master"),
                    publishingTargetDatabase,
                    PublishMode.SingleItem,
                    publishingTargetLanguage,
                    DateTime.MinValue);

                publishingCandidates.Add(new PublishingCandidate(itemId, publishOptions));
                Log.Info($"Added item {item.Paths.FullPath} to the publish queue, target language: {publishingTargetLanguage}, targetDatabase: {publishingTargetDatabase}", new object());
            }
        }

        return publishingCandidates;
    }

    public static void ProcessCandidates(Database sourceDatabase, Database[] targetDatabases, Language[] targetLanguages, List<PublishingCandidate> publishingCandidates, bool skipEvents = false, bool useSecurityDisabler = true)
    {
        Assert.IsNotNull(sourceDatabase, "sourceDatabase");
        Assert.IsNotNull(targetDatabases, "targetDatabases");
        Assert.IsNotNull(targetLanguages, "targetLanguages");

        Log.Info($"{publishingCandidates.Count} items added to publish queue", new object());

        var triggerDatabase = targetDatabases.First();
        var triggerLanguage = targetLanguages.First();

        var defaultPublishOptions = new PublishOptions(
                sourceDatabase,
                triggerDatabase, // need to pass database to satisfy contructor
                PublishMode.Incremental,
                triggerLanguage, // need to pass language to satisfy contructor
                DateTime.Now);

        var publishContext = PublishManager.CreatePublishContext(defaultPublishOptions);

        // required or null exception thrown
        publishContext.Languages = targetLanguages;

        publishContext.Queue.Add(publishingCandidates);

        if (skipEvents)
        {
            var queue = new ProcessQueue();
            Log.Info("Processing Publish Queue, skipEvents = true", new object());

            if (useSecurityDisabler)
            {
                Log.Info("Processing Publish Queue, security disabled", new object());
                using (new SecurityDisabler())
                    queue.Process(publishContext);
            }
            else
            {
                Log.Info("Processing Publish Queue, security enabled", new object());
                queue.Process(publishContext);
            }
        }
        else
        {
            Log.Info("Processing Publish Queue, skipEvents = false", new object());

            if (useSecurityDisabler)
            {
                Log.Info("Processing Publish Queue, security disabled", new object());
                using (new SecurityDisabler())
                    CorePipeline.Run("publish", publishContext);
            }
            else
            {
                Log.Info("Processing Publish Queue, security enabled", new object());
                CorePipeline.Run("publish", publishContext);
            }
            Log.Info("Raising publish:end event", new object());
            global::Sitecore.Events.Event.RaiseEvent("publish:end", new Publisher(defaultPublishOptions));
            Log.Info("Raising publish:end:remote event", new object());
            global::Sitecore.Events.Event.RaiseEvent("publish:end:remote", new Publisher(defaultPublishOptions));
        }
        Log.Info("Finished processing Publish Queue", new object());
    }
}

To add this action to a TDS project, follow the guidelines supplied by TDS.

We have found this action to perform significantly better than the native solution. There are two optional parameters to disable events or to run without SecurityDisabler. Disabling events will slightly improve performance if the events in publish:end or publish:end:remote are not required to run for package installations.

John Rappel

Technical Lead
Tags
  • Efficiency
  • Sitecore
subscribe to GeekHive's newsletter

Never miss out on a Sitecore update again!

Our newsletter features a blog roundup of our top posts so you can stay up to date with industry trends, tutorials, and best practices.

Recent Work

Check out what else we've been working on