Skip to content
This repository was archived by the owner on Jan 10, 2019. It is now read-only.

Add a multistage recipe #61

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@
tags
pkg
spec/spec.html
vendor/cache
Gemfile.lock
3 changes: 2 additions & 1 deletion LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ Copyright 2011,

* Scott Robinson <[email protected]>
* Andrew Kiellor <[email protected]>
* Sam Gibson <[email protected]>
* ThoughtWorks, Inc.

Licensed under the Apache License, Version 2.0 (the "License"); you may not use
Expand All @@ -27,4 +28,4 @@ Depends upon:
* [sinatra http://www.sinatrarb.com/], by Blake Mizerany, under the MIT license.
* [thin http://code.macournoyer.com/thin/], by Marc-Andre Cournoyer, under the Ruby license.
* [trollop http://trollop.rubyforge.org/], by William Morgan, et al., under the Ruby license.
* [uuid https://github.com/assaf/uuid], by Assaf Arkin and Eric Hodel, under the MIT license.
* [uuid https://github.com/assaf/uuid], by Assaf Arkin and Eric Hodel, under the MIT license.
20 changes: 12 additions & 8 deletions bin/bt-go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Builds ready stages until there aren't any.
Usage:
\tbt-go
EOS
opt :once, 'Build only the next ready stage'
opt :once, 'Build only the next ready stage that has not already built'
opt :commit, 'Build the specified commit', :default => 'HEAD'
opt :directory, 'Change to DIRECTORY before doing anything.', :short => :c, :type => :string, :default => Dir.pwd
opt :stage, 'Specify a specific stage to build', :type => :string
Expand All @@ -29,22 +29,26 @@ stage_definition = YAML.load(`#{find_command :stages} --commit #{opts[:commit]}

pipeline = BT::Pipeline.new commit, stage_definition

def build(stage)
stage.build
puts "#{stage.name}: #{stage.result}"
end

if opts[:stage]
stage = pipeline.stages.detect { |s| s.name == opts[:stage] } or raise "No stage #{opts[:stage].inspect} for commit #{commit.sha}"

if stage.done?
puts "#{stage} is already done"
elsif stage.ready?
stage.build
build(stage)
else
raise "#{stage} is not ready, it's blocked by #{stage.blockers.map(&:name).join(', ')}"
end

puts "#{stage.name}: #{stage.result}"
elsif opts[:once]
stage = pipeline.ready.reject(&:done?).first
stage.andand { |s| build(s) } or raise "No stage is ready but unbuilt"
else
while (stage = pipeline.ready.shuffle.first)
stage.build
puts "#{stage.name}: #{stage.result}"
break if opts[:once]
pipeline.walk do |stage|
build(stage)
end
end
4 changes: 2 additions & 2 deletions bt.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ Gem::Specification.new do |s|
s.name = BT::NAME
s.version = BT::VERSION
s.platform = Gem::Platform::RUBY
s.authors = ["Scott Robinson", "Andrew Kiellor"]
s.email = ["[email protected]", "[email protected]"]
s.authors = ["Scott Robinson", "Andrew Kiellor", "Sam Gibson"]
s.email = ["[email protected]", "[email protected]", "[email protected]"]
s.homepage = ""
s.summary = %q{bt is to continuous integration as git is to version control.}
s.description = %q{bt is to continuous integration as git is to version control.}
Expand Down
198 changes: 198 additions & 0 deletions doc/cookbook/2-multistage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
# Recipe 2: A Multi-Stage Build.

For our next dish, we'll prepare an rspec burrito with a dash of cucumber guacamole.

## Ingredients

* A terminal
* Ruby 1.9.2
* rspec
* cucumber

## Preparation

First we need a new project:

$ mkdir -p burrito/spec
$ cd burrito
$ git init

And of course, any project needs some files. Using your favorite editor open up `burrito.rb`:

```ruby
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens when GitHub flavoured Markdown hits other Markdown parsers?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like shit and breaks syntax highlighting. You prefer code blocks as block quotes? It strikes me a silly to worry about other markdown renderers at the moment.

class Burrito
attr_reader :ingredients

def add(ingredient)
@ingredients ||= []
@ingredients << ingredient
end
end
```

And do the same for `spec/burrito_spec.rb`:

```ruby
require "./burrito"

describe Burrito do
let(:burrito) { Burrito.new }

describe "#add" do
it "should add a single ingredient to a delicious list" do
burrito.add "cilantro"
burrito.ingredients.should include "cilantro"
end

it "should add multiple ingredients to a delicious list" do
burrito.add "cilantro"
burrito.add "queso"

burrito.ingredients.should include "cilantro"
burrito.ingredients.should include "queso"
end
end
end
```

Now that some files have been added, let's go ahead and commit them.

$ git add .
$ git commit -m "Start the delicious burrito project"

Yay! Our delicious burrito project is underway. Now we've gotta get our `bt` chefs to start cooking those delicious burritos.

To do that, we'll create a `bt` build stage that runs our spec files. The stage won't produce any files and it doesn't rely on any other stages, so it only needs to know what to run.

$ mkdir stages
$ echo 'run: rspec spec' > stages/rspecs
$ git add stages
$ git commit -m "Added the 'rspecs' stage"
$ bt-go
..

Finished in 0.00104 seconds
2 examples, 0 failures
rspecs: PASS bt loves you (f60c4ca0461dc2a96673fb9252da8a32b1472cc6)

Awesome-sauce (¡que linda salsa!) Our build work and it commited the output of the rspec command as the commit message.

We still need to add that cucumber guacamole that we promised, though. First we'll add the cukes themselves. Let's create the `features` directory and put an acceptance test in it.

$ mkdir features

Edit a simple feature file `features/burrito_cooking_101.feature`:

```ruby
Feature: Burrito building
In order to build a more perfect burrito
As a burrito consumer
I want to add delicious ingredients

Scenario: Add ingredients
Given I have a burrito
When I add "cucumber guacamole" to it
Then the burrito should have "cucumber guacamole" in it
```

Create a steps file `features/burrito_cooking_101_steps.rb`:

```ruby
require './burrito'

Given /^I have a burrito$/ do
@burrito = Burrito.new
end

When /^I add "([^"]*)" to it$/ do |ingredient|
@burrito.add(ingredient)
end

Then /^the burrito should have "([^"]*)" in it$/ do |ingredient|
@burrito.ingredients.should include ingredient
end
```

Commit everything

$ git add features
$ git commit -m "Adding cukes to the kitchen"

Nearly there, we need to add a new stage to the build. This is done by simply adding another file to the stages directory.

The names of stages are (by default) simply the file name of that stage. In this case there's `stages/rspecs` and we'd prefer not to execute our acceptance tests unless our specs pass. That's simple enough to do:

$ cat > stages/cukes <<EOF
needs:
- rspecs
run: cucumber
EOF
$ git add stages/cukes
$ git commit -m "Added the 'cukes' stage"
$ bt-go
..

Finished in 0.00105 seconds
2 examples, 0 failures
rspecs: PASS bt loves you (11d0b0fe0ad327376293c24c4f18a37c17b8ef97)
Feature: Burrito building
In order to build a more perfect burrito
As a burrito consumer
I want to add delicious ingredients

Scenario: Add ingredients # features/burrito_cooking_101.feature:6
Given I have a burrito # features/burrito_cooking_101_steps.rb:3
When I add "cucumber guacamole" to it # features/burrito_cooking_101_steps.rb:7
Then the burrito should have "cucumber guacamole" in it # features/burrito_cooking_101_steps.rb:11

1 scenario (1 passed)
3 steps (3 passed)
0m0.002s
cukes: PASS bt loves you (abc584d900f18f96d40e61df99c9a6482b69a012)

So what happens when a stage fails?

Edit `burrito.rb` so that our specs start failing...

```ruby
class Burrito
attr_reader :ingredients

def add(ingredient)
@ingredients ||= []
@ingredients << "cilantro" # Sólo el cilantro es importante !!!11one
end
end
```

$ git commit -a -m "Nom nom nom nom -- Evil Dr. Salazar"
$ bt-go
.F

Failures:

1) Burrito#add should add multiple ingredients to a delicious list
Failure/Error: burrito.ingredients.should include "queso"
expected ["cilantro", "cilantro"] to include "queso"
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see this as a failure, personally.

Diff:
@@ -1,2 +1,2 @@
-queso
+["cilantro", "cilantro"]
# ./spec/burrito_spec.rb:16:in `block (3 levels) in <top (required)>'

Finished in 0.00157 seconds
2 examples, 1 failure
rspecs: FAIL bt loves you (0637d88b74adb9ce9d2bf5d0d04977c9a12452af)

Notice that `bt` still created a commit for the failure, but it didn't try to run the cukes stage. We can try to run it manually...

$ bt-go --stage cukes
bin/bt-go:40:in `<top (required)>': 8a65d143693c812fd0be759a75202555724693e3/cukes is not ready, it's blocked by rspecs (RuntimeError)
from bin/bt-go:19:in `load'
from bin/bt-go:19:in `<main>'

But, of course, it requires the rspecs step to have finished successfully and so it won't run.

## What's Next?

Dynamically generating stages.
3 changes: 3 additions & 0 deletions doc/stories.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
I am a rockstar developer
I want to be able to run my build multiple times for a given commit
Because sometimes environment failures happen and builds really mean "green" when they say "red"
15 changes: 11 additions & 4 deletions lib/bt/pipeline.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,16 @@

module BT
class Pipeline < Struct.new(:commit, :stage_definition)
def walk &block
topo_sorted_stages = stages
while (stage = topo_sorted_stages.shift)
block.call stage
end
end

def stages
unknown_stages = self[:stage_definition].dup
Set.new.tap do |known_stages|
Array.new.tap do |known_stages|
while !unknown_stages.empty?
name, definition = next_satisfied! unknown_stages, known_stages

Expand All @@ -30,9 +37,9 @@ def to_hash
end

def status
return 'FAIL' if stages.any?(&:fail?)
return 'PASS' if stages.all?(&:ok?)
return 'INCOMPLETE' if stages.any?(&:ok?)
return 'FAIL' if stages.any?(&:failed?)
return 'PASS' if stages.all?(&:passed?)
return 'INCOMPLETE' if stages.any?(&:passed?)
'UNKNOWN'
end

Expand Down
8 changes: 4 additions & 4 deletions lib/bt/stage.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,11 @@ def initialize(commit, name, specification)
@run = Command.new self[:run]
end

def ok?
def passed?
result.andand { |r| r.message.start_with?('PASS') }
end

def fail?
def failed?
result.andand { |r| r.message.start_with?('FAIL') }
end

Expand All @@ -62,11 +62,11 @@ def result
end

def blockers
needs.select { |n| !n.ok? }
needs.reject(&:passed?)
end

def ready?
needs.all?(&:ok?) && !done?
needs.all?(&:passed?)
end

def to_hash
Expand Down
16 changes: 12 additions & 4 deletions spec/bt/stage_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,14 @@
let(:message) { 'PASS bt loves you' }

it { should be_done }
it { should be_ok }
it { should be_passed }
end

context "which was a fail" do
let(:message) { 'FAIL bt loves you' }

it { should be_done }
it { should_not be_ok }
it { should_not be_passed }
end
end

Expand All @@ -46,15 +46,23 @@
end

context "with satisfied needs" do
let(:needs) { [mock(:stage, :ok? => true), mock(:stage, :ok? => true)] }
let(:needs) { [mock(:stage, :passed? => true), mock(:stage, :passed? => true)] }

it { should be_ready }

it "should have no blockers" do
subject.blockers.should== []
end
end

context "with unsatisfied needs" do
let(:needs) { [mock(:stage, :ok? => true), mock(:stage, :ok? => false)] }
let(:needs) { [mock(:stage, :passed? => true), mock(:stage, :passed? => false)] }

it { should_not be_ready }

it "should have blockers" do
subject.blockers.should== needs[1..-1]
end
end
end

Loading