I'm really loving ThoughtWorks Go and Ruby/Rake/Albacore. I'd really suggest checking out this stack for your CI/CD environment. One thing I wanted to do was configure CI with a manual option to push a gem to rubygems.org. This is very easy with the stack just mentioned.

Go Configuration

First we will need to tweak the Go setup to facilitate this. By default the Go server and agent(s) run under the local system account. We want to instead run them under a service account so that we can cache the rubygems.org api key in the user profile. As an aside, you'll see "Cruise" peppered throughout these instructions; Go used to be called Cruise and still retains this name in some areas. So create an account for these services called GoService (Or whatever you want):

image

Set the service account on the "Cruise Server" and "Cruise Agent" services to be the service account you just created (Or agents if you have many):

image

Next, grant this service account full control of the "%PROGRAM_FILES%\Cruise Server" and "%PROGRAM_FILES%\Cruise Agent" folders:

image

Now restart the services and test your build. By default this account will be in the local Users group which may or may not have sufficient permissions for your build. If not then you'll obviously have to grant the additional permissions required by your build to this account. This is good practice anyways.

Now we'll need to add a gem credentials file to the service accounts' user profile directory. When we push the gem in our build script it will use the api key stored in this file to authenticate. You can obtain this key by browsing to your rubygems.org profile page:

image 

Under the service accounts' user profile folder (Usually C:\Users\<Username>) create a folder called .gem (You will have to use the command line to do this as Explorer doesn't like the name) and a file under that folder called credentials:

image

Copy and paste the snippet from the rubygems.org profile page (In the dark brown section), paste it into the credentials file and save it.

image

Now the build server is configured to do the push.

Generating the Gem

I'm not going to get into how to write Rake scripts, see here for more info on that. In order to create a gem you will need to create a gem spec. You can find more info about that here. Here is a simple gem spec:

spec = Gem::Specification.new do |spec|
    spec.platform = Gem::Platform::RUBY
    spec.summary = "Goodies for .NET WCF Rest"
    spec.name = "wcfrestcontrib"
    spec.version = "#{ENV['GO_PIPELINE_LABEL']}"
    spec.files = Dir["lib/**/*"] + Dir["docs/**/*"]
    spec.authors = ["Mike O'Brien"]
    spec.homepage = "http://github.com/mikeobrien/WcfRestContrib"
    spec.description = "The WCF REST Contrib library adds functionality to the current .NET WCF REST implementation."
end

I think the above spec is pretty self explanatory but I'll point out a couple of things. First, the name of the gem (spec.name = "wcfrestcontrib") must be available on the gem server (Just do a search on rubygems.org to check this) and you shouldn't change it after you create it. This is the handle developers will use to identify your gem, so if you go changing it, users may not realize this and continue to use the old handle (Which will no longer be updated). Secondly, we are setting the version of the gem (spec.version = "#{ENV['GO_PIPELINE_LABEL']}"). This is important as this is how the gem server knows what to return when a user asks for the latest gem and it differentiates one version of a gem from another. I'm managing the version number via the build label in Go. I have this set to automatically increment the build number using the COUNT macro (And I can manually modify the major and minor versions when needed). Here is the pipeline configuration in Go:

<pipeline ... labeltemplate="1.0.6.${COUNT}">
    ...
</pipeline>

The third thing I'd like to point out is how to specify the files you want included in the gem (spec.files = Dir["lib/**/*"] + Dir["docs/**/*"]). There is a particular folder layout you will need in in the gem in order for it to work properly. For example binaries or code must be in a folder called "lib" and documents in a folder called "docs". The tricky thing is that when you specify the files, the folder structure in the gem will be based on the path you specify. So lets say you want to add the binaries of one of the projects in your solution. You could say something like this "spec.files = Dir["myproject/bin/release/**/*"]" but then the actual folder structure in the gem will be "/myproject/bin/release/<Your Binaries>"; not going to work. One way to handle this is to create a temporary folder that has the folder structure you want and then copy the files into it. Then you can specify the files for the gem from that folder. Here is how we can accomplish this:

desc "Prepares the gem files to be packaged."
task :prepareGemFiles => :build do
    
    gem = "gem"
    lib = "#{gem}/files/lib"
    docs = "#{gem}/files/docs"
    pkg = "#{gem}/pkg"
    
    if Dir.exists?(gem) then 
         FileUtils.rm_rf gem
    end

    FileUtils.mkdir_p(lib)
    FileUtils.mkdir_p(pkg)
    FileUtils.mkdir_p(docs)
    
    Dir.glob("src/WcfRestContrib/bin/Release/*") do |name|
        FileUtils.cp(name, lib)
    end    
    
    Dir.glob("src/docs/**/*") do |name|
        FileUtils.cp(name, docs)
    end    
    
end

This task generates the following folder structure and copies necessary files into that folder structure:

image

Now the following task will create the gem, referencing the files in our temporary folder "gem/files" (Instead of elsewhere in our project structure):

desc "Creates gem"
task :createGem => :prepareGemFiles do

    FileUtils.cd("gem/files") do
    
        spec = Gem::Specification.new do |spec|
            spec.platform = Gem::Platform::RUBY
            spec.summary = "Goodies for .NET WCF Rest"
            spec.name = "wcfrestcontrib"
            spec.version = "#{ENV['GO_PIPELINE_LABEL']}"
            spec.files = Dir["lib/**/*"] + Dir["docs/**/*"]
            spec.authors = ["Mike O'Brien"]
            spec.homepage = "http://github.com/mikeobrien/WcfRestContrib"
            spec.description = "The WCF REST Contrib library adds functionality to the current .NET WCF REST implementation."
        end

        Rake::GemPackageTask.new(spec) do |package|
            package.package_dir = "../pkg"
        end
        
        Rake::Task["package"].invoke
    end
end

You'll notice that the first thing it does is make the current directory "gem/files". This is important because the paths we specify in the gem spec must be relative to the current directory. We put the code into a block because the cd method will change the current directory only for the code in the block then set the current directory back when the block completes. The Rake::GemPackageTask call dynamically generates some Rake tasks related to gem packages using the gem spec we pass in. The dynamically generated task we're interested in is "package". This task creates our gem and puts it in "gem/pkg" folder (package.package_dir = "../pkg").

As an aside, most examples show the gem spec definition (spec = Gem::Specification...) and gem task creation (Rake::GemPackageTask...) in the body of the script (Not in a Rake task). The reason that we are encapsulating it into a task is because we want to specify the files we want in the gem spec (spec.files = Dir["lib/**/*"] + Dir["docs/**/*"]) after depended-on tasks have run. If we create the spec in the body of the script it immediately executes and pulls the file list before any tasks are run. This is no good because it will not pick up binaries or other items generated by the build task or other tasks. So we want to delay this until all depended-on tasks are run. Plus in this example we also need to make sure we have a prepared folder structure in place to pull our files from so that they are laid out correctly in the gem.

The last thing we'll need to do is push the gem. This is pretty straight forward:

desc "Push the gem to ruby gems"
task :pushGem => :createGem do
    result = system("gem", "push", "gem/pkg/wcfrestcontrib-#{ENV['GO_PIPELINE_LABEL']}.gem")
end
One thing to notice in the tasks we've created are the dependencies. The :pushGem task relies on the :createGem task which relies on the :prepareGemFiles task and so on. We can run the :pushGem task and be certain that all required tasks will be run before hand.   

The entire Rake file can be found here: http://gist.github.com/578564

Go Pipeline Configuration

Now that we have our build script prepared we want to configure Go to have a CI stage and a manual gem push stage. The following pipeline configuration does this:

<pipeline name="WCFRestContrib" labeltemplate="1.0.6.${COUNT}">
  <materials>
    <git url="git://github.com/mikeobrien/WcfRestContrib.git" />
  </materials>
  <stage name="CI">
    <jobs>
      <job name="Default">
        <tasks>
          <rake buildfile="release\rakefile.rb" target="build" />
        </tasks>
      </job>
    </jobs>
  </stage>
  <stage name="PushGem">
    <approval type="manual" />
    <jobs>
      <job name="Default">
        <tasks>
          <rake buildfile="release\rakefile.rb" target="pushGem" />
        </tasks>
      </job>
    </jobs>
  </stage>
</pipeline>

Here in the pipeline element we see the build label template discussed earlier from which our version is generated. For demonstration purposes the CI stage simply builds the project. This stage automatically runs every time we commit to the central repo. The next stage pushes the gem. You'll notice it this has an approval type of manual. This means that you have to explicitly run the next stage. It allows you to choose which version you want pushed to the gem server. Here is what this would look like in the Go UI:

image

Clicking the icon between the two stages brings up a confirmation dialog verifying that you want to run the next stage in the pipeline.

image

This gem push stage was successful; version 1.0.6.81 was pushed out.

Summary

Go + Ruby/Rake/Albacore provides a powerful stack for CI and CD. On thing I really like about Go is the build pipeline. You can create multiple stages that either run automatically when previous stages are complete. Or in the case of deploying to a staging or production environment you can manually run these stages. The price is right for Go as well since ThoughtWorks offers a free community edition.