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 ADDED
@@ -0,0 +1,8 @@
1
+ *.gem
2
+ /.bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ .DS_Store
6
+ *.rbc
7
+ /doc
8
+ .yardoc
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in state_machine_logger.gemspec
4
+ gemspec
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,2 @@
1
+ require File.expand_path('./tasks/github_gem', File.dirname(__FILE__))
2
+ GithubGem::RakeTasks.new
@@ -0,0 +1,2 @@
1
+ # To keep Rails happy
2
+ require 'state_machine/audit_trail'
@@ -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,5 @@
1
+ class StateMachine::AuditTrail::Base < Struct.new(:transition_class)
2
+ def log(object, event, from, to, timestamp = Time.now)
3
+ raise NotImplemented, "Implement in a subclass."
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class StateMachine::AuditTrail::Railtie < ::Rails::Railtie
2
+ generators do
3
+ require 'state_machine/audit_trail_generator'
4
+ end
5
+ 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
@@ -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
@@ -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