state_machine-audit_trail 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +8 -0
- data/Gemfile +4 -0
- data/LICENSE +20 -0
- data/README.rdoc +37 -0
- data/Rakefile +2 -0
- data/lib/state_machine-audit_trail.rb +2 -0
- data/lib/state_machine/audit_trail.rb +21 -0
- data/lib/state_machine/audit_trail/active_record.rb +11 -0
- data/lib/state_machine/audit_trail/base.rb +5 -0
- data/lib/state_machine/audit_trail/railtie.rb +5 -0
- data/lib/state_machine/audit_trail/transition_logging.rb +32 -0
- data/lib/state_machine/audit_trail_generator.rb +21 -0
- data/spec/spec_helper.rb +92 -0
- data/spec/state_machine/audit_trail_spec.rb +75 -0
- data/state_machine-audit_trail.gemspec +23 -0
- data/tasks/github_gem.rb +365 -0
- metadata +146 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Copyright (c) 2011 Jesse Storimer & Willem van Bergen
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
a copy of this software and associated documentation files (the
|
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be
|
|
12
|
+
included in all copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
= StateMachine audit trail
|
|
2
|
+
|
|
3
|
+
The plugin for the state machine gem (see https://github.com/pluginaweek/state_machine) adds support for keeping an audit trail for any state machine. Having an audit trail gives you a complete history of the state changes in your model. This history allows you to investigate incidents or perform analytics, like: "How long does it take on average to go from state a to state b?", or "What percentage of cases goes from state a to b via state c?"
|
|
4
|
+
|
|
5
|
+
Note: while the state_machine gem integrates with multiple ORMs, this plugin currently only has an ActiveRecord backend. It should be easy to add support for other ActiveModel-based ORMs though.
|
|
6
|
+
|
|
7
|
+
== Usage
|
|
8
|
+
|
|
9
|
+
First, make the gem available by adding it to your <tt>Gemfile</tt>, and run <tt>bundle install</tt>:
|
|
10
|
+
|
|
11
|
+
gem 'state_machine-audit_trail'
|
|
12
|
+
|
|
13
|
+
Create a model/table that holds the audit trail. The table needs to have a foreign key to the original object, am "event" field, a "from" state field, a "to" state field, and a "created_at" timestamp that stores the timestamp of the transition. This gem comes with a Rails 3 generator to create a model and a migration like that.
|
|
14
|
+
|
|
15
|
+
rails generate state_machine:audit_trail <model> <state_attribute>
|
|
16
|
+
|
|
17
|
+
For a model called "Model", and a state attribute "state", this will generate the ModelStateTransition model and an accompanying migration.
|
|
18
|
+
|
|
19
|
+
Next, tell your state machine you want to keep an audit trail:
|
|
20
|
+
|
|
21
|
+
class Model < ActiveRecord::Base
|
|
22
|
+
state_machine :state, :initial => :start do
|
|
23
|
+
log_transitions
|
|
24
|
+
...
|
|
25
|
+
|
|
26
|
+
If your audit trail model does not use the default naming scheme, provide it using the <tt>:to</tt> option:
|
|
27
|
+
|
|
28
|
+
class Model < ActiveRecord::Base
|
|
29
|
+
state_machine :state, :initial => :start do
|
|
30
|
+
log_transitions :to => 'ModelAuditTrail'
|
|
31
|
+
...
|
|
32
|
+
|
|
33
|
+
That's it! The plugin will register an <tt>after_transition</tt> callback that is used to log all transitions. It will also log the initial state if there is one.
|
|
34
|
+
|
|
35
|
+
== About
|
|
36
|
+
|
|
37
|
+
This plugin is written by Jesse Storimer and Willem van Bergen for Shopify. It is released under the MIT license (see LICENSE)
|
data/Rakefile
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
require 'state_machine'
|
|
2
|
+
|
|
3
|
+
module StateMachine::AuditTrail
|
|
4
|
+
|
|
5
|
+
VERSION = "0.0.1"
|
|
6
|
+
|
|
7
|
+
def self.setup
|
|
8
|
+
StateMachine::Machine.send(:include, StateMachine::AuditTrail::TransitionLogging)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.create(transition_class)
|
|
12
|
+
return ActiveRecord.new(transition_class) if transition_class < ::ActiveRecord::Base
|
|
13
|
+
raise NotImplemented, "Only support for ActiveRecord is included at this time"
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
require 'state_machine/audit_trail/transition_logging'
|
|
18
|
+
require 'state_machine/audit_trail/base'
|
|
19
|
+
require 'state_machine/audit_trail/active_record'
|
|
20
|
+
require 'state_machine/audit_trail/railtie' if defined?(::Rails)
|
|
21
|
+
StateMachine::AuditTrail.setup
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
class StateMachine::AuditTrail::ActiveRecord < StateMachine::AuditTrail::Base
|
|
2
|
+
def log(object, event, from, to, timestamp = Time.now)
|
|
3
|
+
# Let ActiveRecord manage the timestamp for us so it does the
|
|
4
|
+
# right thing width regards to timezones.
|
|
5
|
+
transition_class.create(foreign_key_field(object) => object.id, :event => event, :from => from, :to => to)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def foreign_key_field(object)
|
|
9
|
+
object.class.name.foreign_key.to_sym
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
module StateMachine::AuditTrail::TransitionLogging
|
|
2
|
+
attr_accessor :transition_class_name
|
|
3
|
+
|
|
4
|
+
def log_transitions(options = {})
|
|
5
|
+
state_machine = self
|
|
6
|
+
state_machine.transition_class_name = (options[:to] || default_transition_class_name).to_s
|
|
7
|
+
|
|
8
|
+
state_machine.after_transition do |object, transition|
|
|
9
|
+
state_machine.audit_trail.log(object, transition.event, transition.from, transition.to)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
state_machine.owner_class.after_create do |object|
|
|
13
|
+
if !object.send(state_machine.attribute).nil?
|
|
14
|
+
state_machine.audit_trail.log(object, nil, nil, object.send(state_machine.attribute))
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def audit_trail
|
|
20
|
+
@transition_logger ||= StateMachine::AuditTrail.create(transition_class)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def transition_class
|
|
26
|
+
@transition_class ||= transition_class_name.constantize
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def default_transition_class_name
|
|
30
|
+
"#{owner_class.name}#{attribute.to_s.camelize}Transition"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
require 'rails/generators'
|
|
2
|
+
|
|
3
|
+
class StateMachine::AuditTrailGenerator < ::Rails::Generators::Base
|
|
4
|
+
|
|
5
|
+
source_root File.join(File.dirname(__FILE__), 'templates')
|
|
6
|
+
|
|
7
|
+
argument :source_model
|
|
8
|
+
argument :state_attribute, :default => 'state'
|
|
9
|
+
argument :transition_model, :default => nil
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def create_model
|
|
13
|
+
Rails::Generators.invoke('model', [transition_class_name, "#{source_model.tableize}:references", "event:string", "from:string", "to:string", "created_at:timestamp", '--no-timestamps'])
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
protected
|
|
17
|
+
|
|
18
|
+
def transition_class_name
|
|
19
|
+
transition_model || "#{source_model.camelize}#{state_attribute.camelize}Transition"
|
|
20
|
+
end
|
|
21
|
+
end
|
data/spec/spec_helper.rb
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
require 'rubygems'
|
|
2
|
+
require 'bundler/setup'
|
|
3
|
+
|
|
4
|
+
require 'rspec'
|
|
5
|
+
require 'active_record'
|
|
6
|
+
require 'state_machine/audit_trail'
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Setup test database
|
|
10
|
+
|
|
11
|
+
ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :database => ':memory:')
|
|
12
|
+
|
|
13
|
+
ActiveRecord::Base.connection.create_table(:test_models) do |t|
|
|
14
|
+
t.string :state
|
|
15
|
+
t.timestamps
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
ActiveRecord::Base.connection.create_table(:test_model_with_multiple_state_machines) do |t|
|
|
19
|
+
t.string :first
|
|
20
|
+
t.string :second
|
|
21
|
+
t.timestamps
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def create_transition_table(owner_class, state)
|
|
27
|
+
class_name = "#{owner_class.name}#{state.to_s.camelize}Transition"
|
|
28
|
+
|
|
29
|
+
ActiveRecord::Base.connection.create_table(class_name.tableize) do |t|
|
|
30
|
+
t.integer owner_class.name.foreign_key
|
|
31
|
+
t.string :event
|
|
32
|
+
t.string :from
|
|
33
|
+
t.string :to
|
|
34
|
+
t.datetime :created_at
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# We probably want to provide a generator for this model and the accompanying migration.
|
|
40
|
+
class TestModelStateTransition < ActiveRecord::Base
|
|
41
|
+
belongs_to :test_model
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
class TestModelWithMultipleStateMachinesFirstTransition < ActiveRecord::Base
|
|
45
|
+
belongs_to :test_model
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
class TestModelWithMultipleStateMachinesSecondTransition < ActiveRecord::Base
|
|
49
|
+
belongs_to :test_model
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class TestModel < ActiveRecord::Base
|
|
54
|
+
|
|
55
|
+
state_machine :state, :initial => :waiting do # log initial state?
|
|
56
|
+
log_transitions
|
|
57
|
+
|
|
58
|
+
event :start do
|
|
59
|
+
transition [:waiting, :stopped] => :started
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
event :stop do
|
|
63
|
+
transition :started => :stopped
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
class TestModelWithMultipleStateMachines < ActiveRecord::Base
|
|
69
|
+
|
|
70
|
+
state_machine :first, :initial => :beginning do
|
|
71
|
+
log_transitions
|
|
72
|
+
|
|
73
|
+
event :begin_first do
|
|
74
|
+
transition :beginning => :end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
state_machine :second do
|
|
79
|
+
log_transitions
|
|
80
|
+
|
|
81
|
+
event :begin_second do
|
|
82
|
+
transition nil => :beginning
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
create_transition_table(TestModel, :state)
|
|
88
|
+
create_transition_table(TestModelWithMultipleStateMachines, :first)
|
|
89
|
+
create_transition_table(TestModelWithMultipleStateMachines, :second)
|
|
90
|
+
|
|
91
|
+
RSpec.configure do |config|
|
|
92
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe StateMachine::AuditTrail do
|
|
4
|
+
it "should have a VERSION constant" do
|
|
5
|
+
StateMachine::AuditTrail.const_defined?('VERSION').should be_true
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
it "should log an event with all fields set correctly" do
|
|
9
|
+
m = TestModel.create!
|
|
10
|
+
m.start!
|
|
11
|
+
last_transition = TestModelStateTransition.where(:test_model_id => m.id).last
|
|
12
|
+
|
|
13
|
+
last_transition.event.should == 'start'
|
|
14
|
+
last_transition.from.should == 'waiting'
|
|
15
|
+
last_transition.to.should == 'started'
|
|
16
|
+
last_transition.created_at.should be_within(10.seconds).of(Time.now.utc)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it "should log multiple events" do
|
|
20
|
+
m = TestModel.create!
|
|
21
|
+
|
|
22
|
+
lambda do
|
|
23
|
+
m.start
|
|
24
|
+
m.stop
|
|
25
|
+
m.start
|
|
26
|
+
end.should change(TestModelStateTransition, :count).by(3)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it "should do nothing when the transition is not exectuted successfully" do
|
|
30
|
+
m = TestModel.create!
|
|
31
|
+
lambda { m.stop }.should_not change(TestModelStateTransition, :count)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it "should log a state_machine specific event for the affected state machine" do
|
|
35
|
+
m = TestModelWithMultipleStateMachines.create!
|
|
36
|
+
lambda { m.begin_first! }.should change(TestModelWithMultipleStateMachinesFirstTransition, :count).by(1)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it "should not log a state_machine specific event for the unaffected state machine" do
|
|
40
|
+
m = TestModelWithMultipleStateMachines.create!
|
|
41
|
+
lambda { m.begin_first! }.should_not change(TestModelWithMultipleStateMachinesSecondTransition, :count)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it "should log a transition for the inital state" do
|
|
45
|
+
lambda { TestModelWithMultipleStateMachines.create! }.should change(TestModelWithMultipleStateMachinesFirstTransition, :count).by(1)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it "should only log the :to state for an initial state" do
|
|
49
|
+
TestModelWithMultipleStateMachines.create!
|
|
50
|
+
initial_transition = TestModelWithMultipleStateMachinesFirstTransition.last
|
|
51
|
+
initial_transition.event.should be_nil
|
|
52
|
+
initial_transition.from.should be_nil
|
|
53
|
+
initial_transition.to.should == 'beginning'
|
|
54
|
+
initial_transition.created_at.should be_within(10.seconds).of(Time.now.utc)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
it "should not log a transition when the state machine does not have an initial state" do
|
|
58
|
+
lambda { TestModelWithMultipleStateMachines.create! }.should_not change(TestModelWithMultipleStateMachinesSecondTransition, :count)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it "should create a transiction for the first record when the state machine does not have an initial state" do
|
|
62
|
+
m = TestModelWithMultipleStateMachines.create!
|
|
63
|
+
lambda { m.begin_second! }.should change(TestModelWithMultipleStateMachinesSecondTransition, :count).by(1)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it "should not have a value for the from state when the state machine does not have an initial state" do
|
|
67
|
+
m = TestModelWithMultipleStateMachines.create!
|
|
68
|
+
m.begin_second!
|
|
69
|
+
first_transition = TestModelWithMultipleStateMachinesSecondTransition.last
|
|
70
|
+
first_transition.event.should == 'begin_second'
|
|
71
|
+
first_transition.from.should be_nil
|
|
72
|
+
first_transition.to.should == 'beginning'
|
|
73
|
+
first_transition.created_at.should be_within(10.seconds).of(Time.now.utc)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
|
2
|
+
Gem::Specification.new do |s|
|
|
3
|
+
s.name = "state_machine-audit_trail"
|
|
4
|
+
s.version = "0.0.1"
|
|
5
|
+
s.platform = Gem::Platform::RUBY
|
|
6
|
+
s.authors = ["Willem van Bergen", "Jesse Storimer"]
|
|
7
|
+
s.email = ["willem@shopify.com", "jesse.storimer@shopify.com"]
|
|
8
|
+
s.homepage = "https://github.com/wvanbergen/state_machine-audit_trail"
|
|
9
|
+
s.summary = %q{Log transitions on a state machine to support auditing and business process analytics.}
|
|
10
|
+
s.description = %q{Log transitions on a state machine to support auditing and business process analytics.}
|
|
11
|
+
|
|
12
|
+
s.rubyforge_project = "state_machine"
|
|
13
|
+
|
|
14
|
+
s.add_runtime_dependency('state_machine')
|
|
15
|
+
|
|
16
|
+
s.add_development_dependency('rake')
|
|
17
|
+
s.add_development_dependency('rspec', '~> 2')
|
|
18
|
+
s.add_development_dependency('activerecord', '~> 3')
|
|
19
|
+
s.add_development_dependency('sqlite3')
|
|
20
|
+
|
|
21
|
+
s.files = %w(.gitignore Gemfile LICENSE README.rdoc Rakefile lib/state_machine-audit_trail.rb lib/state_machine/audit_trail.rb lib/state_machine/audit_trail/active_record.rb lib/state_machine/audit_trail/base.rb lib/state_machine/audit_trail/railtie.rb lib/state_machine/audit_trail/transition_logging.rb lib/state_machine/audit_trail_generator.rb spec/spec_helper.rb spec/state_machine/audit_trail_spec.rb state_machine-audit_trail.gemspec tasks/github_gem.rb)
|
|
22
|
+
s.test_files = %w(spec/state_machine/audit_trail_spec.rb)
|
|
23
|
+
end
|
data/tasks/github_gem.rb
ADDED
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
require 'rubygems'
|
|
2
|
+
require 'rake'
|
|
3
|
+
require 'rake/tasklib'
|
|
4
|
+
require 'date'
|
|
5
|
+
require 'set'
|
|
6
|
+
|
|
7
|
+
module GithubGem
|
|
8
|
+
|
|
9
|
+
# Detects the gemspc file of this project using heuristics.
|
|
10
|
+
def self.detect_gemspec_file
|
|
11
|
+
FileList['*.gemspec'].first
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Detects the main include file of this project using heuristics
|
|
15
|
+
def self.detect_main_include
|
|
16
|
+
if File.exist?(File.expand_path("../lib/#{File.basename(detect_gemspec_file, '.gemspec').gsub(/-/, '/')}.rb", detect_gemspec_file))
|
|
17
|
+
"lib/#{File.basename(detect_gemspec_file, '.gemspec').gsub(/-/, '/')}.rb"
|
|
18
|
+
elsif FileList['lib/*.rb'].length == 1
|
|
19
|
+
FileList['lib/*.rb'].first
|
|
20
|
+
else
|
|
21
|
+
nil
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
class RakeTasks
|
|
26
|
+
|
|
27
|
+
attr_reader :gemspec, :modified_files
|
|
28
|
+
attr_accessor :gemspec_file, :task_namespace, :main_include, :root_dir, :spec_pattern, :test_pattern, :remote, :remote_branch, :local_branch
|
|
29
|
+
|
|
30
|
+
# Initializes the settings, yields itself for configuration
|
|
31
|
+
# and defines the rake tasks based on the gemspec file.
|
|
32
|
+
def initialize(task_namespace = :gem)
|
|
33
|
+
@gemspec_file = GithubGem.detect_gemspec_file
|
|
34
|
+
@task_namespace = task_namespace
|
|
35
|
+
@main_include = GithubGem.detect_main_include
|
|
36
|
+
@modified_files = Set.new
|
|
37
|
+
@root_dir = Dir.pwd
|
|
38
|
+
@test_pattern = 'test/**/*_test.rb'
|
|
39
|
+
@spec_pattern = 'spec/**/*_spec.rb'
|
|
40
|
+
@local_branch = 'master'
|
|
41
|
+
@remote = 'origin'
|
|
42
|
+
@remote_branch = 'master'
|
|
43
|
+
|
|
44
|
+
yield(self) if block_given?
|
|
45
|
+
|
|
46
|
+
load_gemspec!
|
|
47
|
+
define_tasks!
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
protected
|
|
51
|
+
|
|
52
|
+
def git
|
|
53
|
+
@git ||= ENV['GIT'] || 'git'
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Define Unit test tasks
|
|
57
|
+
def define_test_tasks!
|
|
58
|
+
require 'rake/testtask'
|
|
59
|
+
|
|
60
|
+
namespace(:test) do
|
|
61
|
+
Rake::TestTask.new(:basic) do |t|
|
|
62
|
+
t.pattern = test_pattern
|
|
63
|
+
t.verbose = true
|
|
64
|
+
t.libs << 'test'
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
desc "Run all unit tests for #{gemspec.name}"
|
|
69
|
+
task(:test => ['test:basic'])
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Defines RSpec tasks
|
|
73
|
+
def define_rspec_tasks!
|
|
74
|
+
require 'rspec/core/rake_task'
|
|
75
|
+
|
|
76
|
+
namespace(:spec) do
|
|
77
|
+
desc "Verify all RSpec examples for #{gemspec.name}"
|
|
78
|
+
RSpec::Core::RakeTask.new(:basic) do |t|
|
|
79
|
+
t.pattern = spec_pattern
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
desc "Verify all RSpec examples for #{gemspec.name} and output specdoc"
|
|
83
|
+
RSpec::Core::RakeTask.new(:specdoc) do |t|
|
|
84
|
+
t.pattern = spec_pattern
|
|
85
|
+
t.rspec_opts = ['--format', 'documentation', '--color']
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
desc "Run RCov on specs for #{gemspec.name}"
|
|
89
|
+
RSpec::Core::RakeTask.new(:rcov) do |t|
|
|
90
|
+
t.pattern = spec_pattern
|
|
91
|
+
t.rcov = true
|
|
92
|
+
t.rcov_opts = ['--exclude', '"spec/*,gems/*"', '--rails']
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
desc "Verify all RSpec examples for #{gemspec.name} and output specdoc"
|
|
97
|
+
task(:spec => ['spec:specdoc'])
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Defines the rake tasks
|
|
101
|
+
def define_tasks!
|
|
102
|
+
|
|
103
|
+
define_test_tasks! if has_tests?
|
|
104
|
+
define_rspec_tasks! if has_specs?
|
|
105
|
+
|
|
106
|
+
namespace(@task_namespace) do
|
|
107
|
+
desc "Updates the filelist in the gemspec file"
|
|
108
|
+
task(:manifest) { manifest_task }
|
|
109
|
+
|
|
110
|
+
desc "Builds the .gem package"
|
|
111
|
+
task(:build => :manifest) { build_task }
|
|
112
|
+
|
|
113
|
+
desc "Sets the version of the gem in the gemspec"
|
|
114
|
+
task(:set_version => [:check_version, :check_current_branch]) { version_task }
|
|
115
|
+
task(:check_version => :fetch_origin) { check_version_task }
|
|
116
|
+
|
|
117
|
+
task(:fetch_origin) { fetch_origin_task }
|
|
118
|
+
task(:check_current_branch) { check_current_branch_task }
|
|
119
|
+
task(:check_clean_status) { check_clean_status_task }
|
|
120
|
+
task(:check_not_diverged => :fetch_origin) { check_not_diverged_task }
|
|
121
|
+
|
|
122
|
+
checks = [:check_current_branch, :check_clean_status, :check_not_diverged, :check_version]
|
|
123
|
+
checks.unshift('spec:basic') if has_specs?
|
|
124
|
+
checks.unshift('test:basic') if has_tests?
|
|
125
|
+
# checks.push << [:check_rubyforge] if gemspec.rubyforge_project
|
|
126
|
+
|
|
127
|
+
desc "Perform all checks that would occur before a release"
|
|
128
|
+
task(:release_checks => checks)
|
|
129
|
+
|
|
130
|
+
release_tasks = [:release_checks, :set_version, :build, :github_release, :gemcutter_release]
|
|
131
|
+
# release_tasks << [:rubyforge_release] if gemspec.rubyforge_project
|
|
132
|
+
|
|
133
|
+
desc "Release a new version of the gem using the VERSION environment variable"
|
|
134
|
+
task(:release => release_tasks) { release_task }
|
|
135
|
+
|
|
136
|
+
namespace(:release) do
|
|
137
|
+
desc "Release the next version of the gem, by incrementing the last version segment by 1"
|
|
138
|
+
task(:next => [:next_version] + release_tasks) { release_task }
|
|
139
|
+
|
|
140
|
+
desc "Release the next version of the gem, using a patch increment (0.0.1)"
|
|
141
|
+
task(:patch => [:next_patch_version] + release_tasks) { release_task }
|
|
142
|
+
|
|
143
|
+
desc "Release the next version of the gem, using a minor increment (0.1.0)"
|
|
144
|
+
task(:minor => [:next_minor_version] + release_tasks) { release_task }
|
|
145
|
+
|
|
146
|
+
desc "Release the next version of the gem, using a major increment (1.0.0)"
|
|
147
|
+
task(:major => [:next_major_version] + release_tasks) { release_task }
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# task(:check_rubyforge) { check_rubyforge_task }
|
|
151
|
+
# task(:rubyforge_release) { rubyforge_release_task }
|
|
152
|
+
task(:gemcutter_release) { gemcutter_release_task }
|
|
153
|
+
task(:github_release => [:commit_modified_files, :tag_version]) { github_release_task }
|
|
154
|
+
task(:tag_version) { tag_version_task }
|
|
155
|
+
task(:commit_modified_files) { commit_modified_files_task }
|
|
156
|
+
|
|
157
|
+
task(:next_version) { next_version_task }
|
|
158
|
+
task(:next_patch_version) { next_version_task(:patch) }
|
|
159
|
+
task(:next_minor_version) { next_version_task(:minor) }
|
|
160
|
+
task(:next_major_version) { next_version_task(:major) }
|
|
161
|
+
|
|
162
|
+
desc "Updates the gem release tasks with the latest version on Github"
|
|
163
|
+
task(:update_tasks) { update_tasks_task }
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Updates the files list and test_files list in the gemspec file using the list of files
|
|
168
|
+
# in the repository and the spec/test file pattern.
|
|
169
|
+
def manifest_task
|
|
170
|
+
# Load all the gem's files using "git ls-files"
|
|
171
|
+
repository_files = `#{git} ls-files`.split("\n")
|
|
172
|
+
test_files = Dir[test_pattern] + Dir[spec_pattern]
|
|
173
|
+
|
|
174
|
+
update_gemspec(:files, repository_files)
|
|
175
|
+
update_gemspec(:test_files, repository_files & test_files)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Builds the gem
|
|
179
|
+
def build_task
|
|
180
|
+
sh "gem build -q #{gemspec_file}"
|
|
181
|
+
Dir.mkdir('pkg') unless File.exist?('pkg')
|
|
182
|
+
sh "mv #{gemspec.name}-#{gemspec.version}.gem pkg/#{gemspec.name}-#{gemspec.version}.gem"
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def newest_version
|
|
186
|
+
`#{git} tag`.split("\n").map { |tag| tag.split('-').last }.compact.map { |v| Gem::Version.new(v) }.max || Gem::Version.new('0.0.0')
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def next_version(increment = nil)
|
|
190
|
+
next_version = newest_version.segments
|
|
191
|
+
increment_index = case increment
|
|
192
|
+
when :micro then 3
|
|
193
|
+
when :patch then 2
|
|
194
|
+
when :minor then 1
|
|
195
|
+
when :major then 0
|
|
196
|
+
else next_version.length - 1
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
next_version[increment_index] ||= 0
|
|
200
|
+
next_version[increment_index] = next_version[increment_index].succ
|
|
201
|
+
((increment_index + 1)...next_version.length).each { |i| next_version[i] = 0 }
|
|
202
|
+
|
|
203
|
+
Gem::Version.new(next_version.join('.'))
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def next_version_task(increment = nil)
|
|
207
|
+
ENV['VERSION'] = next_version(increment).version
|
|
208
|
+
puts "Releasing version #{ENV['VERSION']}..."
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Updates the version number in the gemspec file, the VERSION constant in the main
|
|
212
|
+
# include file and the contents of the VERSION file.
|
|
213
|
+
def version_task
|
|
214
|
+
update_gemspec(:version, ENV['VERSION']) if ENV['VERSION']
|
|
215
|
+
update_gemspec(:date, Date.today)
|
|
216
|
+
|
|
217
|
+
update_version_file(gemspec.version)
|
|
218
|
+
update_version_constant(gemspec.version)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def check_version_task
|
|
222
|
+
raise "#{ENV['VERSION']} is not a valid version number!" if ENV['VERSION'] && !Gem::Version.correct?(ENV['VERSION'])
|
|
223
|
+
proposed_version = Gem::Version.new((ENV['VERSION'] || gemspec.version).dup)
|
|
224
|
+
raise "This version (#{proposed_version}) is not higher than the highest tagged version (#{newest_version})" if newest_version >= proposed_version
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Checks whether the current branch is not diverged from the remote branch
|
|
228
|
+
def check_not_diverged_task
|
|
229
|
+
raise "The current branch is diverged from the remote branch!" if `#{git} rev-list HEAD..#{remote}/#{remote_branch}`.split("\n").any?
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# Checks whether the repository status ic clean
|
|
233
|
+
def check_clean_status_task
|
|
234
|
+
raise "The current working copy contains modifications" if `#{git} ls-files -m`.split("\n").any?
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Checks whether the current branch is correct
|
|
238
|
+
def check_current_branch_task
|
|
239
|
+
raise "Currently not on #{local_branch} branch!" unless `#{git} branch`.split("\n").detect { |b| /^\* / =~ b } == "* #{local_branch}"
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Fetches the latest updates from Github
|
|
243
|
+
def fetch_origin_task
|
|
244
|
+
sh git, 'fetch', remote
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# Commits every file that has been changed by the release task.
|
|
248
|
+
def commit_modified_files_task
|
|
249
|
+
really_modified = `#{git} ls-files -m #{modified_files.entries.join(' ')}`.split("\n")
|
|
250
|
+
if really_modified.any?
|
|
251
|
+
really_modified.each { |file| sh git, 'add', file }
|
|
252
|
+
sh git, 'commit', '-m', "Released #{gemspec.name} gem version #{gemspec.version}."
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Adds a tag for the released version
|
|
257
|
+
def tag_version_task
|
|
258
|
+
sh git, 'tag', '-a', "#{gemspec.name}-#{gemspec.version}", '-m', "Released #{gemspec.name} gem version #{gemspec.version}."
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Pushes the changes and tag to github
|
|
262
|
+
def github_release_task
|
|
263
|
+
sh git, 'push', '--tags', remote, remote_branch
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def gemcutter_release_task
|
|
267
|
+
sh "gem", 'push', "pkg/#{gemspec.name}-#{gemspec.version}.gem"
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# Gem release task.
|
|
271
|
+
# All work is done by the task's dependencies, so just display a release completed message.
|
|
272
|
+
def release_task
|
|
273
|
+
puts
|
|
274
|
+
puts "Release successful."
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
private
|
|
278
|
+
|
|
279
|
+
# Checks whether this project has any RSpec files
|
|
280
|
+
def has_specs?
|
|
281
|
+
FileList[spec_pattern].any?
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# Checks whether this project has any unit test files
|
|
285
|
+
def has_tests?
|
|
286
|
+
FileList[test_pattern].any?
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# Loads the gemspec file
|
|
290
|
+
def load_gemspec!
|
|
291
|
+
@gemspec = eval(File.read(@gemspec_file))
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# Updates the VERSION file with the new version
|
|
295
|
+
def update_version_file(version)
|
|
296
|
+
if File.exists?('VERSION')
|
|
297
|
+
File.open('VERSION', 'w') { |f| f << version.to_s }
|
|
298
|
+
modified_files << 'VERSION'
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# Updates the VERSION constant in the main include file if it exists
|
|
303
|
+
def update_version_constant(version)
|
|
304
|
+
if main_include && File.exist?(main_include)
|
|
305
|
+
file_contents = File.read(main_include)
|
|
306
|
+
if file_contents.sub!(/^(\s+VERSION\s*=\s*)[^\s].*$/) { $1 + version.to_s.inspect }
|
|
307
|
+
File.open(main_include, 'w') { |f| f << file_contents }
|
|
308
|
+
modified_files << main_include
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
# Updates an attribute of the gemspec file.
|
|
314
|
+
# This function will open the file, and search/replace the attribute using a regular expression.
|
|
315
|
+
def update_gemspec(attribute, new_value, literal = false)
|
|
316
|
+
|
|
317
|
+
unless literal
|
|
318
|
+
new_value = case new_value
|
|
319
|
+
when Array then "%w(#{new_value.join(' ')})"
|
|
320
|
+
when Hash, String then new_value.inspect
|
|
321
|
+
when Date then new_value.strftime('%Y-%m-%d').inspect
|
|
322
|
+
else raise "Cannot write value #{new_value.inspect} to gemspec file!"
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
spec = File.read(gemspec_file)
|
|
327
|
+
regexp = Regexp.new('^(\s+\w+\.' + Regexp.quote(attribute.to_s) + '\s*=\s*)[^\s].*$')
|
|
328
|
+
if spec.sub!(regexp) { $1 + new_value }
|
|
329
|
+
File.open(gemspec_file, 'w') { |f| f << spec }
|
|
330
|
+
modified_files << gemspec_file
|
|
331
|
+
|
|
332
|
+
# Reload the gemspec so the changes are incorporated
|
|
333
|
+
load_gemspec!
|
|
334
|
+
|
|
335
|
+
# Also mark the Gemfile.lock file as changed because of the new version.
|
|
336
|
+
modified_files << 'Gemfile.lock' if File.exist?(File.join(root_dir, 'Gemfile.lock'))
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
# Updates the tasks file using the latest file found on Github
|
|
341
|
+
def update_tasks_task
|
|
342
|
+
require 'net/https'
|
|
343
|
+
require 'uri'
|
|
344
|
+
|
|
345
|
+
uri = URI.parse('https://github.com/wvanbergen/github-gem/raw/master/tasks/github-gem.rake')
|
|
346
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
347
|
+
http.use_ssl = true
|
|
348
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
|
349
|
+
response = http.request(Net::HTTP::Get.new(uri.path))
|
|
350
|
+
|
|
351
|
+
if Net::HTTPSuccess === response
|
|
352
|
+
open(__FILE__, "w") { |file| file.write(response.body) }
|
|
353
|
+
relative_file = File.expand_path(__FILE__).sub(%r[^#{@root_dir}/], '')
|
|
354
|
+
if `#{git} ls-files -m #{relative_file}`.split("\n").any?
|
|
355
|
+
sh git, 'add', relative_file
|
|
356
|
+
sh git, 'commit', '-m', "Updated to latest gem release management tasks."
|
|
357
|
+
else
|
|
358
|
+
puts "Release managament tasks already are at the latest version."
|
|
359
|
+
end
|
|
360
|
+
else
|
|
361
|
+
raise "Download failed with HTTP status #{response.code}!"
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: state_machine-audit_trail
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
prerelease: false
|
|
5
|
+
segments:
|
|
6
|
+
- 0
|
|
7
|
+
- 0
|
|
8
|
+
- 1
|
|
9
|
+
version: 0.0.1
|
|
10
|
+
platform: ruby
|
|
11
|
+
authors:
|
|
12
|
+
- Willem van Bergen
|
|
13
|
+
- Jesse Storimer
|
|
14
|
+
autorequire:
|
|
15
|
+
bindir: bin
|
|
16
|
+
cert_chain: []
|
|
17
|
+
|
|
18
|
+
date: 2011-03-15 00:00:00 -04:00
|
|
19
|
+
default_executable:
|
|
20
|
+
dependencies:
|
|
21
|
+
- !ruby/object:Gem::Dependency
|
|
22
|
+
name: state_machine
|
|
23
|
+
prerelease: false
|
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
|
25
|
+
none: false
|
|
26
|
+
requirements:
|
|
27
|
+
- - ">="
|
|
28
|
+
- !ruby/object:Gem::Version
|
|
29
|
+
segments:
|
|
30
|
+
- 0
|
|
31
|
+
version: "0"
|
|
32
|
+
type: :runtime
|
|
33
|
+
version_requirements: *id001
|
|
34
|
+
- !ruby/object:Gem::Dependency
|
|
35
|
+
name: rake
|
|
36
|
+
prerelease: false
|
|
37
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
|
38
|
+
none: false
|
|
39
|
+
requirements:
|
|
40
|
+
- - ">="
|
|
41
|
+
- !ruby/object:Gem::Version
|
|
42
|
+
segments:
|
|
43
|
+
- 0
|
|
44
|
+
version: "0"
|
|
45
|
+
type: :development
|
|
46
|
+
version_requirements: *id002
|
|
47
|
+
- !ruby/object:Gem::Dependency
|
|
48
|
+
name: rspec
|
|
49
|
+
prerelease: false
|
|
50
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
|
51
|
+
none: false
|
|
52
|
+
requirements:
|
|
53
|
+
- - ~>
|
|
54
|
+
- !ruby/object:Gem::Version
|
|
55
|
+
segments:
|
|
56
|
+
- 2
|
|
57
|
+
version: "2"
|
|
58
|
+
type: :development
|
|
59
|
+
version_requirements: *id003
|
|
60
|
+
- !ruby/object:Gem::Dependency
|
|
61
|
+
name: activerecord
|
|
62
|
+
prerelease: false
|
|
63
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
|
64
|
+
none: false
|
|
65
|
+
requirements:
|
|
66
|
+
- - ~>
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
segments:
|
|
69
|
+
- 3
|
|
70
|
+
version: "3"
|
|
71
|
+
type: :development
|
|
72
|
+
version_requirements: *id004
|
|
73
|
+
- !ruby/object:Gem::Dependency
|
|
74
|
+
name: sqlite3
|
|
75
|
+
prerelease: false
|
|
76
|
+
requirement: &id005 !ruby/object:Gem::Requirement
|
|
77
|
+
none: false
|
|
78
|
+
requirements:
|
|
79
|
+
- - ">="
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
segments:
|
|
82
|
+
- 0
|
|
83
|
+
version: "0"
|
|
84
|
+
type: :development
|
|
85
|
+
version_requirements: *id005
|
|
86
|
+
description: Log transitions on a state machine to support auditing and business process analytics.
|
|
87
|
+
email:
|
|
88
|
+
- willem@shopify.com
|
|
89
|
+
- jesse.storimer@shopify.com
|
|
90
|
+
executables: []
|
|
91
|
+
|
|
92
|
+
extensions: []
|
|
93
|
+
|
|
94
|
+
extra_rdoc_files: []
|
|
95
|
+
|
|
96
|
+
files:
|
|
97
|
+
- .gitignore
|
|
98
|
+
- Gemfile
|
|
99
|
+
- LICENSE
|
|
100
|
+
- README.rdoc
|
|
101
|
+
- Rakefile
|
|
102
|
+
- lib/state_machine-audit_trail.rb
|
|
103
|
+
- lib/state_machine/audit_trail.rb
|
|
104
|
+
- lib/state_machine/audit_trail/active_record.rb
|
|
105
|
+
- lib/state_machine/audit_trail/base.rb
|
|
106
|
+
- lib/state_machine/audit_trail/railtie.rb
|
|
107
|
+
- lib/state_machine/audit_trail/transition_logging.rb
|
|
108
|
+
- lib/state_machine/audit_trail_generator.rb
|
|
109
|
+
- spec/spec_helper.rb
|
|
110
|
+
- spec/state_machine/audit_trail_spec.rb
|
|
111
|
+
- state_machine-audit_trail.gemspec
|
|
112
|
+
- tasks/github_gem.rb
|
|
113
|
+
has_rdoc: true
|
|
114
|
+
homepage: https://github.com/wvanbergen/state_machine-audit_trail
|
|
115
|
+
licenses: []
|
|
116
|
+
|
|
117
|
+
post_install_message:
|
|
118
|
+
rdoc_options: []
|
|
119
|
+
|
|
120
|
+
require_paths:
|
|
121
|
+
- lib
|
|
122
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
123
|
+
none: false
|
|
124
|
+
requirements:
|
|
125
|
+
- - ">="
|
|
126
|
+
- !ruby/object:Gem::Version
|
|
127
|
+
segments:
|
|
128
|
+
- 0
|
|
129
|
+
version: "0"
|
|
130
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
131
|
+
none: false
|
|
132
|
+
requirements:
|
|
133
|
+
- - ">="
|
|
134
|
+
- !ruby/object:Gem::Version
|
|
135
|
+
segments:
|
|
136
|
+
- 0
|
|
137
|
+
version: "0"
|
|
138
|
+
requirements: []
|
|
139
|
+
|
|
140
|
+
rubyforge_project: state_machine
|
|
141
|
+
rubygems_version: 1.3.7
|
|
142
|
+
signing_key:
|
|
143
|
+
specification_version: 3
|
|
144
|
+
summary: Log transitions on a state machine to support auditing and business process analytics.
|
|
145
|
+
test_files:
|
|
146
|
+
- spec/state_machine/audit_trail_spec.rb
|