As with all our projects, we have setup continuous integration on the BigUtilityCorp project. In order to integrate this we needed to be able to deploy the K2 Smart Objects and Workflow Processes to the K2 server on the command line using MSBuild. The following code is an MSBuild task which I created based on some code on the K2Underground site, and extended it.
The biggest hurdle we came across in implementing this commandline MSBuild task was that, whenever you perform a build or deploy of a K2 solution, K2 requires that new class names are generated for it's .kprx and .sodx files and new GUIDs created for the objects deployed to the server. This is done so that K2 can seemlessly version the Smart Object and Workflow Processes in the server, however in our development environment, we aren't worried about being able to support old versions of the Smart Objects or Workflows, so this was not necessary.
The problem that this caused for us was that all our code is stored in TFS and so it's all read-only and cannot be edited unless checked out, however K2 was requiring that we check out all the files and change the class names to allow this versioning. Of course, when running an MSBuild task, I may also have some of the K2 files checked out if I'm working on them, so I don't want to check everything out automatically, or set them all to be writable.
The solution is to make an MSBuild task which takes a copy of the K2 project files, then builds and deploys them, and finally packages up a set of files able to be deployed on another server. We have 2 projects, one for the Smart Objects and one for the Workflow, so we call 2 targets in our MSBuild file from the target DeployK2:
msbuild /t:DeployK2
I should not at this point that the target is also creating a deployable package of the K2 items so it could be more accurately named PackageAndDeployK2, but I chose the less verbose variety.
<Import Condition="" Project="$(MSBuildExtensionsPath)\MyCompany.MSBuild.Tasks.K2.Targets" />
<Target Name="DeployK2">
<K2Deploy Server="$(Computername)" Port="5555"
Environment="Development"
ProjectPath="$(MSBuildProjectDirectory)\SmartObjects.k2proj"
OutputPath="$(MSBuildProjectDirectory)\Package\SmartObjects"/>
<K2Deploy Server="$(Computername)" Port="5555"
Environment="Development"
ProjectPath="$(MSBuildProjectDirectory)\Workflow.k2proj"
OutputPath="$(MSBuildProjectDirectory)\Package\Workflow"/>
</Target>
The MyCompany.MSBuild.Tasks.K2.Targets file is as follows:
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<K2TasksLib>$(MSBuildExtensionsPath)\MyCompany.MSBuild.Tasks.K2.dll</K2TasksLib>
</PropertyGroup>
<UsingTask AssemblyFile="$(K2TasksLib)"
TaskName="MyCompany.MSBuild.Tasks.K2.K2Deploy" />
</Project>
Following is the code for the MSBuild task - You'll need to build it and copy it and the above .targets file into your C:\Program Files\MSBuild folder. Here we copy all of the K2 projects files to a temporary direct, set them all to be writable then run the K2 deploy code to create the packages and deploy to our local K2 server:
using System;
using System.Collections.Generic;
using System.Security.AccessControl;
using System.Text;
using System.IO;
using Microsoft.Build.Utilities;
using Microsoft.Build.Framework;
using SourceCode.EnvironmentSettings.Client;
using SourceCode.Framework.Deployment;
using SourceCode.ProjectSystem;
using SourceCode.Workflow.Design.EnvironmentSettings;
namespace MyCompany.MSBuild.Tasks.K2
{
/// <summary>
/// Author: http://www.danielflippance.com
///
/// This class is an MSBuild task used to deploy a K2 Blackpearl project to a K2 server
/// K2 requires that all files in the K2 project are writable in order to perform a deployment
/// so we copy the entire project folder and all it's containing files to a Temp folder.
/// We then make all those files writable and then perform the deployment.
///
/// Output files, including MSBuild files are copied to the OutputPath
///
/// This code was based on the example at:
/// http://www.k2underground.com/blogs/pitchblack/archive/2008/04/30/automatic-deployment-of-k2-process-and-k2-smartobject-artefacts-using-thesourcecode-deployment-framework-and-msbuild-assemblies.aspx
/// http://www.csharp411.com/c-copy-folder-recursively/
/// http://www.west-wind.com/weblog/posts/4072.aspx
/// </summary>
public class K2Deploy : Task
{
#region Properties
private string server = "localhost";
public string Server
{
get { return server; }
set { server = value; }
}
private int port = 5555;
public int Port
{
get { return port; }
set { port = value; }
}
private string environment = "Development";
public string Environment
{
get { return environment; }
set { environment = value; }
}
/// <summary>
/// The location of the .csproj file containing the K2 SmartObjects or Workflow Processes
/// </summary>
private string projectPath;
[Required]
public string ProjectPath
{
get { return projectPath; }
set { projectPath = value; }
}
/// <summary>
/// The folder name where the output files will be created
/// </summary>
private string outputPath;
[Required]
public string OutputPath
{
get { return outputPath; }
set { outputPath = value; }
}
private string ConnectionString
{
get
{
return string.Format("Integrated=True;IsPrimaryLogin=True;Authenticate=True;EncryptedPassword=False;Host={0};Port={1}",
server, port);
}
}
#endregion
public override bool Execute()
{
Project project;
EnvironmentSettingsManager environmentManager;
DeploymentResults results;
DeploymentPackage package;
bool result = false;
//Create a temporary folder for the K2 project files
string tempPath = Path.GetTempPath().Trim('\\').Trim('/');
string k2DeployFolder = tempPath + @"\K2Deploy";
DeleteDirectory(k2DeployFolder);
try
{
//Check parameters
if (!ProjectPath.EndsWith(".k2proj")) throw new ArgumentException("ProjectPath must end with .k2proj");
//Create a temporary folder for the code
string projectFolder = ProjectPath.Substring(0, ProjectPath.LastIndexOf('\\'));
//Copy the files to the temp folder
Console.WriteLine("Creating temporary folder: " + k2DeployFolder);
CopyFolder(projectFolder, k2DeployFolder);
//Ensure we have access to all the files.
Console.WriteLine("setting writable permissions for folder: " + tempPath);
bool success = SetAcl(k2DeployFolder, "F", true);
if (!success) throw new Exception("Failed to set ACLs on folder " + tempPath);
//Ensure the feils are all writable
SetWritable(k2DeployFolder);
//Load the project file
string newProjectFile = k2DeployFolder + @"\" + ProjectPath.Substring(1 + ProjectPath.LastIndexOf('\\'));
Console.WriteLine("Loading project file: " + newProjectFile);
project = new Project();
project.Load(newProjectFile);
// Compile the K2 Project
Console.WriteLine("Compiling project file: " + newProjectFile);
results = project.Compile();
//Grab the deployment
environmentManager = GetEnvironmentManager();
package = GetDeploymentPackage(project, environmentManager);
Console.WriteLine("Saving deployment package to folder: " + OutputPath);
package.Save(OutputPath, "K2 Deployment Package");
Console.WriteLine("Executing deployment package...");
results = package.Execute();
result = results.Successful;
}
catch (Exception ex)
{
Console.Write(ex.Message);
throw;
}
finally
{
DeleteDirectory(k2DeployFolder);
}
return result;
}
private EnvironmentSettingsManager GetEnvironmentManager()
{
EnvironmentSettingsManager environmentManager = new EnvironmentSettingsManager(false, false);
// This is weird but the only way I could get it to work!
environmentManager.ConnectToServer(ConnectionString);
environmentManager.InitializeSettingsManager();
environmentManager.ConnectToServer(ConnectionString);
if (!string.IsNullOrEmpty(environment))
environmentManager.ChangeEnvironment(environment);
environmentManager.InitializeSettingsManager();
environmentManager.GetEnvironmentFields(environmentManager.CurrentEnvironment);
return environmentManager;
}
private DeploymentPackage GetDeploymentPackage(Project project, EnvironmentSettingsManager environmentManager)
{
DeploymentPackage package;
package = project.CreateDeploymentPackage();
// Populate Environment Fields
foreach (EnvironmentInstance env in environmentManager.CurrentTemplate.Environments)
{
DeploymentEnvironment depEnv = package.AddEnvironment(env.EnvironmentName);
foreach (EnvironmentField field in env.EnvironmentFields)
{
depEnv.Properties[field.FieldName] = field.Value;
}
}
package.SelectedEnvironment = environmentManager.CurrentEnvironment.EnvironmentName;
package.DeploymentLabelName = DateTime.Now.ToString();
package.DeploymentLabelDescription = string.Empty;
package.TestOnly = false;
// Get the Default SmartObject Server in the Environment
// The prefix "$Field=" is when the value of the SmartObject server is registered in the environment fields collection.
// this will do a lookup in the environment with the display name of the field, and use the value.
// If you set the value directly, no lookups will be performed.
EnvironmentField smartObjectServerField =
environmentManager.CurrentEnvironment.GetDefaultField(typeof(SmartObjectField));
package.SmartObjectConnectionString = "$Field=" + smartObjectServerField.DisplayName;
// Get the Default Workflow Management Server in the Environment
EnvironmentField workflowServerField =
environmentManager.CurrentEnvironment.GetDefaultField(typeof(WorkflowManagementServerField));
package.WorkflowManagementConnectionString = "$Field=" + workflowServerField.DisplayName;
return package;
}
/// <summary>
/// Recursively copy the source folder to the destination folder
/// Created destination folder if necessary
/// From: http://www.csharp411.com/c-copy-folder-recursively/
/// </summary>
/// <param name="sourceFolder"></param>
/// <param name="destFolder"></param>
private void CopyFolder(string sourceFolder, string destFolder)
{
if (!Directory.Exists(destFolder))
Directory.CreateDirectory(destFolder);
string[] files = Directory.GetFiles(sourceFolder);
foreach (string file in files)
{
string name = Path.GetFileName(file);
string dest = Path.Combine(destFolder, name);
File.Copy(file, dest);
}
string[] folders = Directory.GetDirectories(sourceFolder);
foreach (string folder in folders)
{
string name = Path.GetFileName(folder);
string dest = Path.Combine(destFolder, name);
CopyFolder(folder, dest);
}
}
/// <summary>
/// Recursively set the ACL on a folder
/// From: http://www.west-wind.com/weblog/posts/4072.aspx
/// </summary>
/// <returns></returns>
private bool SetAcl(string folderName, string userRights, bool inheritSubDirectories)
{
if (folderName == null || folderName == "")
{
Console.WriteLine("Path cannot be empty.");
return false;
}
// *** Strip off trailing backslash which isn't supported
folderName = folderName.TrimEnd('\\');
FileSystemRights rights = (FileSystemRights)0;
if (userRights == "R")
rights = FileSystemRights.ReadAndExecute;
else if (userRights == "C")
rights = FileSystemRights.ChangePermissions;
else if (userRights == "F")
rights = FileSystemRights.FullControl;
// *** Add Access Rule to the actual directory itself
string currentUserName = System.Environment.UserDomainName + @"\" + System.Environment.UserName;
FileSystemAccessRule accessRule = new FileSystemAccessRule(currentUserName, rights,
InheritanceFlags.None,
PropagationFlags.NoPropagateInherit,
AccessControlType.Allow);
DirectoryInfo Info = new DirectoryInfo(folderName);
DirectorySecurity Security = Info.GetAccessControl(AccessControlSections.Access);
bool Result = false;
Security.ModifyAccessRule(AccessControlModification.Set, accessRule, out Result);
if (!Result)
return false;
// *** Always allow objects to inherit on a directory
InheritanceFlags iFlags = InheritanceFlags.ObjectInherit;
if (inheritSubDirectories)
iFlags = InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit;
// *** Add Access rule for the inheritance
accessRule = new FileSystemAccessRule(currentUserName, rights,
iFlags,
PropagationFlags.InheritOnly,
AccessControlType.Allow);
Result = false;
Security.ModifyAccessRule(AccessControlModification.Add, accessRule, out Result);
if (!Result) return false;
Info.SetAccessControl(Security);
return true;
}
private void SetWritable(string folder)
{
foreach (string f in Directory.GetFiles(folder)) File.SetAttributes(f, FileAttributes.Normal);
foreach (string d in Directory.GetDirectories(folder)) SetWritable(d);
}
private void DeleteDirectory(string folder)
{
if (Directory.Exists(folder))
{
foreach (string f in Directory.GetFiles(folder)) File.Delete(f);
foreach (string d in Directory.GetDirectories(folder)) DeleteDirectory(d);
Directory.Delete(folder, true);
}
}
}
}