script_tracker 0.1.3 → 0.2.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +36 -1
- data/lib/script_tracker/base.rb +30 -15
- data/lib/script_tracker/executed_script.rb +63 -0
- data/lib/script_tracker/generators/install_generator.rb +11 -11
- data/lib/script_tracker/railtie.rb +1 -1
- data/lib/script_tracker/version.rb +1 -1
- data/lib/script_tracker.rb +6 -6
- data/tasks/script_tracker.rake +34 -9
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a326d388e66fd43a071121d79a3dec532912917e6fb1f904dcc7092ba4140271
|
|
4
|
+
data.tar.gz: a79e24b9834cbbc9c36c3f3a32b12999ae261a41901b0bacb991ba7ea06af762
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bf6e2f7d6f7ccfc53f7e7e07cbf726305d39c264e7335371a1bfca66774df6839f2b5f7731808b827e6f54eeee276997a69d0ba5aa12b6a3447015e136bc44af
|
|
7
|
+
data.tar.gz: 6aa3f1c42500576c2dfc69b3772151937b258e3490f8a5da2abc77d436bb76611a5ed6344f769cb1ad038649e50220b04d8613b8c4e5da76f694aab9c5b4d8ae
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,40 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.2.0] - 2025-01-17
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **Database advisory locks** for preventing concurrent script execution (PostgreSQL, MySQL, SQLite)
|
|
14
|
+
- `acquire_lock(filename)` - Manual lock acquisition
|
|
15
|
+
- `release_lock(filename)` - Manual lock release
|
|
16
|
+
- `with_advisory_lock(filename)` - Automatic lock management with block
|
|
17
|
+
- `check_timeout!(start_time, max_duration)` - Safer manual timeout checking
|
|
18
|
+
- RuboCop configuration with comprehensive rules
|
|
19
|
+
- RuboCop integration in CI/CD pipeline
|
|
20
|
+
- 14 new test cases for concurrency and timeout behavior
|
|
21
|
+
- Support for multi-database advisory locking strategies
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
- **CRITICAL**: Removed broken pre-execution timeout check that never worked
|
|
25
|
+
- **CRITICAL**: Added concurrency protection to prevent data corruption from simultaneous script runs
|
|
26
|
+
- Fixed `load` usage with proper warning suppression to prevent constant redefinition warnings
|
|
27
|
+
- Fixed nested ternary operators in rake tasks for better readability
|
|
28
|
+
- Fixed line length issues throughout codebase
|
|
29
|
+
- All RuboCop offenses resolved (0 offenses)
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
- Documented risks of Ruby's `Timeout.timeout` with comprehensive warnings
|
|
33
|
+
- Improved code quality across all files (100% RuboCop compliant)
|
|
34
|
+
- Enhanced test coverage from 55 to 69 examples
|
|
35
|
+
- Improved error handling and logging
|
|
36
|
+
- Better numeric predicates usage (`.positive?` instead of `> 0`)
|
|
37
|
+
- Consistent string literal style (single quotes)
|
|
38
|
+
|
|
39
|
+
### Security
|
|
40
|
+
- Concurrent execution protection prevents race conditions and data corruption
|
|
41
|
+
- Advisory locks work across PostgreSQL, MySQL, and SQLite
|
|
42
|
+
- Locks automatically released even on exceptions
|
|
43
|
+
|
|
10
44
|
## [0.1.3] - 2025-01-17
|
|
11
45
|
|
|
12
46
|
### Changed
|
|
@@ -41,7 +75,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
41
75
|
- Rake tasks for managing scripts
|
|
42
76
|
- Comprehensive RSpec test suite
|
|
43
77
|
|
|
44
|
-
[Unreleased]: https://github.com/a-abdellatif98/script_tracker/compare/v0.
|
|
78
|
+
[Unreleased]: https://github.com/a-abdellatif98/script_tracker/compare/v0.2.0...HEAD
|
|
79
|
+
[0.2.0]: https://github.com/a-abdellatif98/script_tracker/compare/v0.1.3...v0.2.0
|
|
45
80
|
[0.1.3]: https://github.com/a-abdellatif98/script_tracker/compare/v0.1.2...v0.1.3
|
|
46
81
|
[0.1.2]: https://github.com/a-abdellatif98/script_tracker/compare/v0.1.1...v0.1.2
|
|
47
82
|
[0.1.1]: https://github.com/a-abdellatif98/script_tracker/compare/v0.1.0...v0.1.1
|
data/lib/script_tracker/base.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
module ScriptTracker
|
|
4
4
|
class Base
|
|
5
5
|
class ScriptSkipped < StandardError; end
|
|
6
|
+
class ScriptTimeoutError < StandardError; end
|
|
6
7
|
|
|
7
8
|
class << self
|
|
8
9
|
# Default timeout: 5 minutes
|
|
@@ -14,23 +15,21 @@ module ScriptTracker
|
|
|
14
15
|
300 # 5 minutes in seconds
|
|
15
16
|
end
|
|
16
17
|
|
|
17
|
-
def run(
|
|
18
|
+
def run(_executed_script_record = nil)
|
|
18
19
|
require 'timeout'
|
|
19
20
|
start_time = Time.current
|
|
20
21
|
timeout_seconds = timeout
|
|
21
22
|
|
|
22
|
-
# Check timeout before execution if we have a record
|
|
23
|
-
if executed_script_record&.timed_out?
|
|
24
|
-
duration = Time.current - start_time
|
|
25
|
-
error_message = "Script timed out after #{timeout_seconds} seconds"
|
|
26
|
-
log(error_message, level: :error)
|
|
27
|
-
return { success: false, skipped: false, output: error_message, duration: duration }
|
|
28
|
-
end
|
|
29
|
-
|
|
30
23
|
begin
|
|
31
24
|
result = nil
|
|
32
25
|
|
|
33
26
|
# Wrap execution in timeout if specified
|
|
27
|
+
# WARNING: Ruby's Timeout.timeout has known issues and can interrupt code at any point,
|
|
28
|
+
# potentially leaving resources in inconsistent states. Consider these alternatives:
|
|
29
|
+
# 1. Implement timeout logic within your script using Time.current checks
|
|
30
|
+
# 2. Use database statement_timeout for PostgreSQL
|
|
31
|
+
# 3. Monitor script execution time and kill from outside if needed
|
|
32
|
+
# This timeout is provided as a last resort safety measure.
|
|
34
33
|
if timeout_seconds && timeout_seconds > 0
|
|
35
34
|
Timeout.timeout(timeout_seconds, ScriptTimeoutError) do
|
|
36
35
|
result = execute_with_transaction
|
|
@@ -48,7 +47,7 @@ module ScriptTracker
|
|
|
48
47
|
duration = Time.current - start_time
|
|
49
48
|
output = e.message.presence || 'Script was skipped (no action needed)'
|
|
50
49
|
{ success: false, skipped: true, output: output, duration: duration }
|
|
51
|
-
rescue ScriptTimeoutError
|
|
50
|
+
rescue ScriptTimeoutError
|
|
52
51
|
duration = Time.current - start_time
|
|
53
52
|
error_message = "Script execution exceeded timeout of #{timeout_seconds} seconds"
|
|
54
53
|
log(error_message, level: :error)
|
|
@@ -68,8 +67,6 @@ module ScriptTracker
|
|
|
68
67
|
end
|
|
69
68
|
end
|
|
70
69
|
|
|
71
|
-
class ScriptTimeoutError < StandardError; end
|
|
72
|
-
|
|
73
70
|
def execute
|
|
74
71
|
raise NotImplementedError, 'Subclasses must implement the execute method'
|
|
75
72
|
end
|
|
@@ -88,14 +85,15 @@ module ScriptTracker
|
|
|
88
85
|
|
|
89
86
|
def log_progress(current, total, message = nil)
|
|
90
87
|
percentage = ((current.to_f / total) * 100).round(2)
|
|
91
|
-
|
|
88
|
+
progress_detail = "(#{current}/#{total} - #{percentage}%)"
|
|
89
|
+
msg = message ? "#{message} #{progress_detail}" : "Progress: #{current}/#{total} (#{percentage}%)"
|
|
92
90
|
log(msg)
|
|
93
91
|
end
|
|
94
92
|
|
|
95
93
|
def process_in_batches(relation, batch_size: 1000, &block)
|
|
96
94
|
total = relation.count
|
|
97
95
|
log("There are #{total} records to process")
|
|
98
|
-
return 0 if total
|
|
96
|
+
return 0 if total == 0
|
|
99
97
|
|
|
100
98
|
processed = 0
|
|
101
99
|
log("Processing #{total} records in batches of #{batch_size}")
|
|
@@ -103,11 +101,28 @@ module ScriptTracker
|
|
|
103
101
|
block.call(record)
|
|
104
102
|
processed += 1
|
|
105
103
|
log_interval = [batch_size, (total * 0.1).to_i].max
|
|
106
|
-
log_progress(processed, total) if (processed % log_interval)
|
|
104
|
+
log_progress(processed, total) if (processed % log_interval) == 0
|
|
107
105
|
end
|
|
108
106
|
log_progress(processed, total, 'Completed')
|
|
109
107
|
processed
|
|
110
108
|
end
|
|
109
|
+
|
|
110
|
+
# Check if execution has exceeded timeout (safer alternative to Timeout.timeout)
|
|
111
|
+
# Use this inside your scripts for manual timeout checking:
|
|
112
|
+
#
|
|
113
|
+
# def self.execute
|
|
114
|
+
# start_time = Time.current
|
|
115
|
+
# User.find_each do |user|
|
|
116
|
+
# check_timeout!(start_time, timeout)
|
|
117
|
+
# # Process user...
|
|
118
|
+
# end
|
|
119
|
+
# end
|
|
120
|
+
def check_timeout!(start_time, max_duration = timeout)
|
|
121
|
+
elapsed = Time.current - start_time
|
|
122
|
+
return unless max_duration&.positive? && elapsed > max_duration
|
|
123
|
+
|
|
124
|
+
raise ScriptTimeoutError, "Script execution exceeded #{max_duration} seconds (elapsed: #{elapsed.round(2)}s)"
|
|
125
|
+
end
|
|
111
126
|
end
|
|
112
127
|
end
|
|
113
128
|
end
|
|
@@ -6,6 +6,7 @@ module ScriptTracker
|
|
|
6
6
|
|
|
7
7
|
# Constants
|
|
8
8
|
DEFAULT_TIMEOUT = 300 # 5 minutes in seconds
|
|
9
|
+
LOCK_KEY_PREFIX = 0x5343525054 # 'SCRPT' in hex for script tracker locks
|
|
9
10
|
|
|
10
11
|
# Validations
|
|
11
12
|
validates :filename, presence: true, uniqueness: true
|
|
@@ -45,6 +46,68 @@ module ScriptTracker
|
|
|
45
46
|
count
|
|
46
47
|
end
|
|
47
48
|
|
|
49
|
+
# Advisory lock methods for preventing concurrent execution
|
|
50
|
+
def self.with_advisory_lock(filename)
|
|
51
|
+
lock_acquired = acquire_lock(filename)
|
|
52
|
+
return { success: false, locked: false } unless lock_acquired
|
|
53
|
+
|
|
54
|
+
begin
|
|
55
|
+
yield
|
|
56
|
+
ensure
|
|
57
|
+
release_lock(filename)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def self.acquire_lock(filename)
|
|
62
|
+
lock_id = generate_lock_id(filename)
|
|
63
|
+
|
|
64
|
+
case connection.adapter_name.downcase
|
|
65
|
+
when 'postgresql'
|
|
66
|
+
# Use PostgreSQL advisory locks (non-blocking)
|
|
67
|
+
result = connection.execute("SELECT pg_try_advisory_lock(#{lock_id})").first
|
|
68
|
+
[true, 't'].include?(result['pg_try_advisory_lock'])
|
|
69
|
+
when 'mysql', 'mysql2', 'trilogy'
|
|
70
|
+
# Use MySQL named locks (timeout: 0 for non-blocking)
|
|
71
|
+
result = connection.execute("SELECT GET_LOCK('script_tracker_#{lock_id}', 0) AS locked").first
|
|
72
|
+
result['locked'] == 1 || result[0] == 1
|
|
73
|
+
else
|
|
74
|
+
# Fallback: use database record with unique constraint
|
|
75
|
+
# This will raise an exception if script is already running
|
|
76
|
+
begin
|
|
77
|
+
exists?(filename: filename, status: 'running') == false
|
|
78
|
+
rescue ActiveRecord::RecordNotUnique
|
|
79
|
+
false
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
rescue StandardError => e
|
|
83
|
+
Rails.logger&.warn("Failed to acquire lock for #{filename}: #{e.message}")
|
|
84
|
+
false
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def self.release_lock(filename)
|
|
88
|
+
lock_id = generate_lock_id(filename)
|
|
89
|
+
|
|
90
|
+
case connection.adapter_name.downcase
|
|
91
|
+
when 'postgresql'
|
|
92
|
+
connection.execute("SELECT pg_advisory_unlock(#{lock_id})")
|
|
93
|
+
when 'mysql', 'mysql2', 'trilogy'
|
|
94
|
+
connection.execute("SELECT RELEASE_LOCK('script_tracker_#{lock_id}')")
|
|
95
|
+
else
|
|
96
|
+
# No-op for fallback strategy
|
|
97
|
+
true
|
|
98
|
+
end
|
|
99
|
+
rescue StandardError => e
|
|
100
|
+
Rails.logger&.warn("Failed to release lock for #{filename}: #{e.message}")
|
|
101
|
+
false
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def self.generate_lock_id(filename)
|
|
105
|
+
# Generate a consistent integer ID from filename for advisory locks
|
|
106
|
+
# Using CRC32 to convert string to integer
|
|
107
|
+
require 'zlib'
|
|
108
|
+
(LOCK_KEY_PREFIX << 32) | (Zlib.crc32(filename) & 0xFFFFFFFF)
|
|
109
|
+
end
|
|
110
|
+
|
|
48
111
|
# Instance methods
|
|
49
112
|
def mark_success!(output_text = nil, execution_duration = nil)
|
|
50
113
|
update!(
|
|
@@ -10,16 +10,16 @@ module ScriptTracker
|
|
|
10
10
|
include Rails::Generators::Migration
|
|
11
11
|
|
|
12
12
|
source_root File.expand_path('templates', __dir__)
|
|
13
|
-
desc
|
|
13
|
+
desc 'Creates ScriptTracker migration file and initializer'
|
|
14
14
|
|
|
15
15
|
class_option :uuid, type: :boolean, default: true,
|
|
16
|
-
|
|
16
|
+
desc: 'Use UUID for primary keys (requires database support)'
|
|
17
17
|
|
|
18
18
|
class_option :skip_migration, type: :boolean, default: false,
|
|
19
|
-
|
|
19
|
+
desc: 'Skip creating the migration file'
|
|
20
20
|
|
|
21
21
|
class_option :skip_initializer, type: :boolean, default: false,
|
|
22
|
-
|
|
22
|
+
desc: 'Skip creating the initializer file'
|
|
23
23
|
|
|
24
24
|
def self.next_migration_number(dirname)
|
|
25
25
|
next_migration_number = current_migration_number(dirname) + 1
|
|
@@ -30,8 +30,8 @@ module ScriptTracker
|
|
|
30
30
|
return if options[:skip_migration]
|
|
31
31
|
|
|
32
32
|
migration_template(
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
'create_executed_scripts.rb.erb',
|
|
34
|
+
'db/migrate/create_executed_scripts.rb',
|
|
35
35
|
migration_version: migration_version
|
|
36
36
|
)
|
|
37
37
|
end
|
|
@@ -39,16 +39,16 @@ module ScriptTracker
|
|
|
39
39
|
def create_initializer
|
|
40
40
|
return if options[:skip_initializer]
|
|
41
41
|
|
|
42
|
-
template
|
|
42
|
+
template 'initializer.rb', 'config/initializers/script_tracker.rb'
|
|
43
43
|
end
|
|
44
44
|
|
|
45
45
|
def create_scripts_directory
|
|
46
|
-
empty_directory
|
|
47
|
-
create_file
|
|
46
|
+
empty_directory 'lib/scripts'
|
|
47
|
+
create_file 'lib/scripts/.keep'
|
|
48
48
|
end
|
|
49
49
|
|
|
50
50
|
def show_readme
|
|
51
|
-
readme
|
|
51
|
+
readme 'README' if behavior == :invoke
|
|
52
52
|
end
|
|
53
53
|
|
|
54
54
|
private
|
|
@@ -62,7 +62,7 @@ module ScriptTracker
|
|
|
62
62
|
end
|
|
63
63
|
|
|
64
64
|
def primary_key_type
|
|
65
|
-
use_uuid? ?
|
|
65
|
+
use_uuid? ? ':uuid' : 'true'
|
|
66
66
|
end
|
|
67
67
|
end
|
|
68
68
|
end
|
|
@@ -9,7 +9,7 @@ module ScriptTracker
|
|
|
9
9
|
Dir.glob("#{path}/../../tasks/**/*.rake").each { |f| load f }
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
-
initializer
|
|
12
|
+
initializer 'script_tracker.configure' do |app|
|
|
13
13
|
app.config.script_tracker = ActiveSupport::OrderedOptions.new
|
|
14
14
|
app.config.script_tracker.scripts_path = app.root.join('lib', 'scripts')
|
|
15
15
|
end
|
data/lib/script_tracker.rb
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
5
|
-
require
|
|
6
|
-
require
|
|
3
|
+
require 'script_tracker/version'
|
|
4
|
+
require 'script_tracker/base'
|
|
5
|
+
require 'script_tracker/executed_script'
|
|
6
|
+
require 'script_tracker/railtie' if defined?(Rails::Railtie)
|
|
7
7
|
|
|
8
8
|
# Load generators for Rails
|
|
9
9
|
if defined?(Rails)
|
|
10
|
-
require
|
|
11
|
-
require_relative
|
|
10
|
+
require 'rails/generators'
|
|
11
|
+
require_relative 'script_tracker/generators/install_generator'
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
module ScriptTracker
|
data/tasks/script_tracker.rake
CHANGED
|
@@ -17,14 +17,15 @@ namespace :scripts do
|
|
|
17
17
|
scripts_dir = ScriptTracker.scripts_path
|
|
18
18
|
file_path = scripts_dir.join(filename)
|
|
19
19
|
|
|
20
|
-
FileUtils.mkdir_p(scripts_dir)
|
|
20
|
+
FileUtils.mkdir_p(scripts_dir)
|
|
21
21
|
|
|
22
22
|
template = File.read(File.expand_path('../templates/script_template.rb', __dir__))
|
|
23
|
+
timestamp_str = Time.current.strftime('%Y-%m-%d %H:%M:%S')
|
|
23
24
|
content = template.gsub('<%= filename %>', filename)
|
|
24
25
|
.gsub('<%= description %>', description)
|
|
25
26
|
.gsub('<%= class_name %>', class_name)
|
|
26
27
|
.gsub('<%= Time.current.year %>', Time.current.year.to_s)
|
|
27
|
-
.gsub('<%= Time.current.strftime(\'%Y-%m-%d %H:%M:%S\') %>',
|
|
28
|
+
.gsub('<%= Time.current.strftime(\'%Y-%m-%d %H:%M:%S\') %>', timestamp_str)
|
|
28
29
|
|
|
29
30
|
File.write(file_path, content)
|
|
30
31
|
|
|
@@ -43,7 +44,7 @@ namespace :scripts do
|
|
|
43
44
|
|
|
44
45
|
unless Dir.exist?(scripts_dir)
|
|
45
46
|
puts "Error: Scripts directory does not exist: #{scripts_dir}"
|
|
46
|
-
puts
|
|
47
|
+
puts 'Please run: rails generate script_tracker:install'
|
|
47
48
|
exit 1
|
|
48
49
|
end
|
|
49
50
|
|
|
@@ -63,9 +64,17 @@ namespace :scripts do
|
|
|
63
64
|
script_name = File.basename(script_path)
|
|
64
65
|
puts "[#{index + 1}/#{pending_scripts.count}] Running #{script_name}..."
|
|
65
66
|
|
|
66
|
-
|
|
67
|
-
|
|
67
|
+
# Try to acquire lock for this script
|
|
68
|
+
lock_result = ScriptTracker::ExecutedScript.with_advisory_lock(script_name) do
|
|
69
|
+
# Load script file using `load` (not `require`) because:
|
|
70
|
+
# 1. Scripts are one-off files that should be loaded fresh each time
|
|
71
|
+
# 2. Allows script modifications to be picked up without restart
|
|
72
|
+
# 3. Scripts are not part of Rails autoload paths
|
|
73
|
+
# Suppress warnings about already initialized constants
|
|
74
|
+
original_verbosity = $VERBOSE
|
|
75
|
+
$VERBOSE = nil
|
|
68
76
|
load script_path
|
|
77
|
+
$VERBOSE = original_verbosity
|
|
69
78
|
|
|
70
79
|
# Extract and validate class name
|
|
71
80
|
class_name = script_name.gsub(/^\d+_/, '').gsub('.rb', '').camelize
|
|
@@ -128,12 +137,22 @@ namespace :scripts do
|
|
|
128
137
|
puts "Error: Syntax error in script #{script_name}: #{e.message}"
|
|
129
138
|
failed_count += 1
|
|
130
139
|
rescue StandardError => e
|
|
131
|
-
duration =
|
|
140
|
+
duration = begin
|
|
141
|
+
(Time.current - start_time)
|
|
142
|
+
rescue StandardError
|
|
143
|
+
0
|
|
144
|
+
end
|
|
132
145
|
error_message = "#{e.class}: #{e.message}"
|
|
133
146
|
executed_script&.mark_failed!(error_message, duration)
|
|
134
147
|
puts "Error: #{e.message}\n\n"
|
|
135
148
|
failed_count += 1
|
|
136
149
|
end
|
|
150
|
+
|
|
151
|
+
# Check if lock was not acquired
|
|
152
|
+
if lock_result == { success: false, locked: false }
|
|
153
|
+
puts "Skipped: Another process is already running #{script_name}\n\n"
|
|
154
|
+
skipped_count += 1
|
|
155
|
+
end
|
|
137
156
|
end
|
|
138
157
|
|
|
139
158
|
puts "Summary: #{success_count} succeeded, #{failed_count} failed, #{skipped_count} skipped"
|
|
@@ -147,7 +166,7 @@ namespace :scripts do
|
|
|
147
166
|
|
|
148
167
|
unless Dir.exist?(scripts_dir)
|
|
149
168
|
puts "Error: Scripts directory does not exist: #{scripts_dir}"
|
|
150
|
-
puts
|
|
169
|
+
puts 'Please run: rails generate script_tracker:install'
|
|
151
170
|
exit 1
|
|
152
171
|
end
|
|
153
172
|
|
|
@@ -156,7 +175,7 @@ namespace :scripts do
|
|
|
156
175
|
|
|
157
176
|
if script_files.empty?
|
|
158
177
|
puts "\nNo scripts found in #{scripts_dir}"
|
|
159
|
-
puts
|
|
178
|
+
puts 'Create a script with: rake scripts:create["description"]'
|
|
160
179
|
exit 0
|
|
161
180
|
end
|
|
162
181
|
|
|
@@ -164,7 +183,13 @@ namespace :scripts do
|
|
|
164
183
|
script_files.each do |file|
|
|
165
184
|
filename = File.basename(file)
|
|
166
185
|
if (script = executed_scripts[filename])
|
|
167
|
-
status_icon = script.success?
|
|
186
|
+
status_icon = if script.success?
|
|
187
|
+
'[SUCCESS]'
|
|
188
|
+
elsif script.failed?
|
|
189
|
+
'[FAILED]'
|
|
190
|
+
else
|
|
191
|
+
script.skipped? ? '[SKIPPED]' : '[RUNNING]'
|
|
192
|
+
end
|
|
168
193
|
puts " #{status_icon} #{filename} (#{script.formatted_duration})"
|
|
169
194
|
else
|
|
170
195
|
puts " [PENDING] #{filename}"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: script_tracker
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ahmed Abd El-Latif
|
|
@@ -124,6 +124,7 @@ metadata:
|
|
|
124
124
|
homepage_uri: https://github.com/a-abdellatif98/script_tracker
|
|
125
125
|
source_code_uri: https://github.com/a-abdellatif98/script_tracker
|
|
126
126
|
changelog_uri: https://github.com/a-abdellatif98/script_tracker/blob/main/CHANGELOG.md
|
|
127
|
+
rubygems_mfa_required: 'true'
|
|
127
128
|
post_install_message:
|
|
128
129
|
rdoc_options: []
|
|
129
130
|
require_paths:
|