state_machine-audit_trail 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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