script_tracker 0.1.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 +7 -0
- data/CHANGELOG.md +30 -0
- data/LICENSE +21 -0
- data/README.md +276 -0
- data/lib/script_tracker/base.rb +113 -0
- data/lib/script_tracker/executed_script.rb +119 -0
- data/lib/script_tracker/generators/install_generator.rb +69 -0
- data/lib/script_tracker/generators/templates/README +22 -0
- data/lib/script_tracker/generators/templates/create_executed_scripts.rb.erb +17 -0
- data/lib/script_tracker/generators/templates/initializer.rb +8 -0
- data/lib/script_tracker/railtie.rb +17 -0
- data/lib/script_tracker/version.rb +5 -0
- data/lib/script_tracker.rb +37 -0
- data/tasks/script_tracker.rake +200 -0
- data/templates/script_template.rb +42 -0
- metadata +146 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 6c4624d6b1fc0ede5e5691a6ec4b2ced79513620505434524b9d1d980e93d4ca
|
|
4
|
+
data.tar.gz: 427a3b57e4fe3436db63864e7a9d323f285c4ad5d1c763c15813a9da92c2d43d
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 776ff4218886ee763f1b93d8212dfae35c68fa2f685f89a2664d642b4934ace4a1a0c3260bd692b7688c3cfc58864dd812329f24161ac91462b5d2ee92cf183a
|
|
7
|
+
data.tar.gz: 1ce7a1b8dc3b65888dbe2e9117b4cb06b3e31d0ff24f195d2e4bdb66d9b1782a2bd2441df09b66ff2aa4e8cb9ace86068bd0bb5af9c81051b2a90d325f1be7cf
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.0] - 2025-01-17
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Initial release of ScriptTracker
|
|
14
|
+
- Script execution tracking with status management
|
|
15
|
+
- Transaction support for script execution
|
|
16
|
+
- Built-in logging and progress tracking
|
|
17
|
+
- Batch processing helpers
|
|
18
|
+
- Timeout support for long-running scripts
|
|
19
|
+
- Stale script cleanup functionality
|
|
20
|
+
- Rake tasks for managing scripts:
|
|
21
|
+
- `scripts:create` - Create new scripts
|
|
22
|
+
- `scripts:run` - Run pending scripts
|
|
23
|
+
- `scripts:status` - View script status
|
|
24
|
+
- `scripts:rollback` - Rollback scripts
|
|
25
|
+
- `scripts:cleanup` - Cleanup stale scripts
|
|
26
|
+
- Comprehensive RSpec test suite
|
|
27
|
+
- Full documentation and examples
|
|
28
|
+
|
|
29
|
+
[Unreleased]: https://github.com/blink-global/script_tracker/compare/v0.1.0...HEAD
|
|
30
|
+
[0.1.0]: https://github.com/blink-global/script_tracker/releases/tag/v0.1.0
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Blink
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
# ScriptTracker
|
|
2
|
+
|
|
3
|
+
ScriptTracker is a Ruby gem that provides a migration-like system for managing one-off scripts in Rails applications. It tracks script execution history, provides transaction support, and includes built-in logging and progress tracking.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Execution Tracking**: Automatically tracks which scripts have been run and their status
|
|
8
|
+
- **Transaction Support**: Wraps script execution in database transactions
|
|
9
|
+
- **Status Management**: Track scripts as success, failed, running, or skipped
|
|
10
|
+
- **Built-in Logging**: Convenient logging methods with timestamps
|
|
11
|
+
- **Batch Processing**: Helper methods for processing large datasets efficiently
|
|
12
|
+
- **Timeout Support**: Configure custom timeouts for long-running scripts
|
|
13
|
+
- **Stale Script Cleanup**: Automatically identify and cleanup stuck scripts
|
|
14
|
+
- **Migration Generator**: Generate timestamped script files with templates
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
Add this line to your application's Gemfile:
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
gem 'script_tracker'
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
And then execute:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
bundle install
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Or install it yourself as:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
gem install script_tracker
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Setup
|
|
37
|
+
|
|
38
|
+
1. Install the gem using the generator:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
rails generate script_tracker:install
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
This will:
|
|
45
|
+
- Create a migration for the `executed_scripts` table
|
|
46
|
+
- Create the `lib/scripts` directory for your scripts
|
|
47
|
+
|
|
48
|
+
2. Run the migration:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
rails db:migrate
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
3. (Optional) Configure the scripts directory in an initializer:
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
# config/initializers/script_tracker.rb
|
|
58
|
+
ScriptTracker.configure do |config|
|
|
59
|
+
config.scripts_path = Rails.root.join('lib', 'scripts')
|
|
60
|
+
end
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Usage
|
|
64
|
+
|
|
65
|
+
### Creating a New Script
|
|
66
|
+
|
|
67
|
+
Generate a new script with a descriptive name:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
rake scripts:create["update user preferences"]
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
This creates a timestamped script file in `lib/scripts/`:
|
|
74
|
+
|
|
75
|
+
```ruby
|
|
76
|
+
# lib/scripts/20231117120000_update_user_preferences.rb
|
|
77
|
+
module Scripts
|
|
78
|
+
class UpdateUserPreferences < ScriptTracker::Base
|
|
79
|
+
def self.execute
|
|
80
|
+
log "Starting script: update user preferences"
|
|
81
|
+
|
|
82
|
+
# Your script logic here
|
|
83
|
+
User.find_each do |user|
|
|
84
|
+
user.update!(preferences: { theme: 'dark' })
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
log "Script completed successfully"
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Running Scripts
|
|
94
|
+
|
|
95
|
+
Run all pending (not yet executed) scripts:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
rake scripts:run
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Checking Script Status
|
|
102
|
+
|
|
103
|
+
View the status of all scripts:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
rake scripts:status
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Output:
|
|
110
|
+
```
|
|
111
|
+
Scripts:
|
|
112
|
+
[SUCCESS] 20231117120000_update_user_preferences.rb (2.5s)
|
|
113
|
+
[PENDING] 20231117130000_cleanup_old_data.rb
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Rolling Back a Script
|
|
117
|
+
|
|
118
|
+
Remove a script from execution history (allows it to be run again):
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
rake scripts:rollback[20231117120000_update_user_preferences.rb]
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Cleaning Up Stale Scripts
|
|
125
|
+
|
|
126
|
+
Mark scripts stuck in "running" status as failed:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
rake scripts:cleanup
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Advanced Features
|
|
133
|
+
|
|
134
|
+
### Skipping Scripts
|
|
135
|
+
|
|
136
|
+
Skip a script execution conditionally:
|
|
137
|
+
|
|
138
|
+
```ruby
|
|
139
|
+
module Scripts
|
|
140
|
+
class ConditionalUpdate < ScriptTracker::Base
|
|
141
|
+
def self.execute
|
|
142
|
+
if User.where(needs_update: true).count.zero?
|
|
143
|
+
skip! "No users need updating"
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Your script logic here
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Custom Timeout
|
|
153
|
+
|
|
154
|
+
Override the default 5-minute timeout:
|
|
155
|
+
|
|
156
|
+
```ruby
|
|
157
|
+
module Scripts
|
|
158
|
+
class LongRunningScript < ScriptTracker::Base
|
|
159
|
+
def self.timeout
|
|
160
|
+
3600 # 1 hour in seconds
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def self.execute
|
|
164
|
+
# Long-running logic here
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Batch Processing
|
|
171
|
+
|
|
172
|
+
Process large datasets efficiently:
|
|
173
|
+
|
|
174
|
+
```ruby
|
|
175
|
+
module Scripts
|
|
176
|
+
class ProcessUsers < ScriptTracker::Base
|
|
177
|
+
def self.execute
|
|
178
|
+
users = User.where(processed: false)
|
|
179
|
+
|
|
180
|
+
process_in_batches(users, batch_size: 1000) do |user|
|
|
181
|
+
user.update!(processed: true)
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Progress Logging
|
|
189
|
+
|
|
190
|
+
Track progress during execution:
|
|
191
|
+
|
|
192
|
+
```ruby
|
|
193
|
+
module Scripts
|
|
194
|
+
class DataMigration < ScriptTracker::Base
|
|
195
|
+
def self.execute
|
|
196
|
+
total = User.count
|
|
197
|
+
processed = 0
|
|
198
|
+
|
|
199
|
+
User.find_each do |user|
|
|
200
|
+
# Process user
|
|
201
|
+
processed += 1
|
|
202
|
+
log_progress(processed, total) if (processed % 100).zero?
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## API Reference
|
|
210
|
+
|
|
211
|
+
### ScriptTracker::Base
|
|
212
|
+
|
|
213
|
+
Base class for all scripts.
|
|
214
|
+
|
|
215
|
+
**Class Methods:**
|
|
216
|
+
|
|
217
|
+
- `execute` - Implement this method with your script logic (required)
|
|
218
|
+
- `run` - Execute the script with transaction and error handling
|
|
219
|
+
- `timeout` - Override to set custom timeout (default: 300 seconds)
|
|
220
|
+
- `skip!(reason)` - Skip script execution with optional reason
|
|
221
|
+
- `log(message, level: :info)` - Log a message with timestamp
|
|
222
|
+
- `log_progress(current, total, message = nil)` - Log progress percentage
|
|
223
|
+
- `process_in_batches(relation, batch_size: 1000, &block)` - Process records in batches
|
|
224
|
+
|
|
225
|
+
### ScriptTracker::ExecutedScript
|
|
226
|
+
|
|
227
|
+
ActiveRecord model for tracking script execution.
|
|
228
|
+
|
|
229
|
+
**Scopes:**
|
|
230
|
+
|
|
231
|
+
- `successful` - Scripts that completed successfully
|
|
232
|
+
- `failed` - Scripts that failed
|
|
233
|
+
- `running` - Scripts currently running
|
|
234
|
+
- `skipped` - Scripts that were skipped
|
|
235
|
+
- `completed` - Scripts that finished (success or failed)
|
|
236
|
+
- `ordered` - Order by execution time ascending
|
|
237
|
+
- `recent_first` - Order by execution time descending
|
|
238
|
+
|
|
239
|
+
**Class Methods:**
|
|
240
|
+
|
|
241
|
+
- `executed?(filename)` - Check if a script has been executed
|
|
242
|
+
- `mark_as_running(filename)` - Mark a script as running
|
|
243
|
+
- `cleanup_stale_running_scripts(older_than: 1.hour.ago)` - Clean up stale scripts
|
|
244
|
+
|
|
245
|
+
**Instance Methods:**
|
|
246
|
+
|
|
247
|
+
- `mark_success!(output, duration)` - Mark as successful
|
|
248
|
+
- `mark_failed!(error, duration)` - Mark as failed
|
|
249
|
+
- `mark_skipped!(output, duration)` - Mark as skipped
|
|
250
|
+
- `success?`, `failed?`, `running?`, `skipped?` - Status predicates
|
|
251
|
+
- `formatted_duration` - Human-readable duration
|
|
252
|
+
- `formatted_output` - Truncated output text
|
|
253
|
+
- `timeout_seconds` - Get timeout value
|
|
254
|
+
- `timed_out?` - Check if script has timed out
|
|
255
|
+
|
|
256
|
+
## Rake Tasks
|
|
257
|
+
|
|
258
|
+
- `rake scripts:create[description]` - Create a new script
|
|
259
|
+
- `rake scripts:run` - Run all pending scripts
|
|
260
|
+
- `rake scripts:status` - Show status of all scripts
|
|
261
|
+
- `rake scripts:rollback[filename]` - Rollback a script
|
|
262
|
+
- `rake scripts:cleanup` - Cleanup stale running scripts
|
|
263
|
+
|
|
264
|
+
## Development
|
|
265
|
+
|
|
266
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests.
|
|
267
|
+
|
|
268
|
+
To install this gem onto your local machine, run `bundle exec rake install`.
|
|
269
|
+
|
|
270
|
+
## Contributing
|
|
271
|
+
|
|
272
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/blink-global/script_tracker.
|
|
273
|
+
|
|
274
|
+
## License
|
|
275
|
+
|
|
276
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ScriptTracker
|
|
4
|
+
class Base
|
|
5
|
+
class ScriptSkipped < StandardError; end
|
|
6
|
+
|
|
7
|
+
class << self
|
|
8
|
+
# Default timeout: 5 minutes
|
|
9
|
+
# Override in subclass to customize:
|
|
10
|
+
# def self.timeout
|
|
11
|
+
# 3600 # 1 hour
|
|
12
|
+
# end
|
|
13
|
+
def timeout
|
|
14
|
+
300 # 5 minutes in seconds
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def run(executed_script_record = nil)
|
|
18
|
+
require 'timeout'
|
|
19
|
+
start_time = Time.current
|
|
20
|
+
timeout_seconds = timeout
|
|
21
|
+
|
|
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
|
+
begin
|
|
31
|
+
result = nil
|
|
32
|
+
|
|
33
|
+
# Wrap execution in timeout if specified
|
|
34
|
+
if timeout_seconds && timeout_seconds > 0
|
|
35
|
+
Timeout.timeout(timeout_seconds, ScriptTimeoutError) do
|
|
36
|
+
result = execute_with_transaction
|
|
37
|
+
end
|
|
38
|
+
else
|
|
39
|
+
result = execute_with_transaction
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
duration = Time.current - start_time
|
|
43
|
+
output = "Script completed successfully in #{duration.round(2)}s"
|
|
44
|
+
log(output)
|
|
45
|
+
|
|
46
|
+
{ success: true, skipped: false, output: output, duration: duration }
|
|
47
|
+
rescue ScriptSkipped => e
|
|
48
|
+
duration = Time.current - start_time
|
|
49
|
+
output = e.message.presence || 'Script was skipped (no action needed)'
|
|
50
|
+
{ success: false, skipped: true, output: output, duration: duration }
|
|
51
|
+
rescue ScriptTimeoutError => e
|
|
52
|
+
duration = Time.current - start_time
|
|
53
|
+
error_message = "Script execution exceeded timeout of #{timeout_seconds} seconds"
|
|
54
|
+
log(error_message, level: :error)
|
|
55
|
+
{ success: false, skipped: false, output: error_message, duration: duration }
|
|
56
|
+
rescue StandardError => e
|
|
57
|
+
duration = Time.current - start_time
|
|
58
|
+
error_message = "#{e.class}: #{e.message}\n#{e.backtrace.first(10).join("\n")}"
|
|
59
|
+
log(error_message, level: :error)
|
|
60
|
+
|
|
61
|
+
{ success: false, skipped: false, output: error_message, duration: duration }
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def execute_with_transaction
|
|
66
|
+
ActiveRecord::Base.transaction do
|
|
67
|
+
execute
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
class ScriptTimeoutError < StandardError; end
|
|
72
|
+
|
|
73
|
+
def execute
|
|
74
|
+
raise NotImplementedError, 'Subclasses must implement the execute method'
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def skip!(reason = nil)
|
|
78
|
+
message = reason ? "Skipping: #{reason}" : 'Skipping script'
|
|
79
|
+
log(message)
|
|
80
|
+
raise ScriptSkipped, message
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def log(message, level: :info)
|
|
84
|
+
timestamp = Time.current.strftime('%Y-%m-%d %H:%M:%S')
|
|
85
|
+
prefix = level == :error ? '[ERROR]' : '[INFO]'
|
|
86
|
+
puts "#{prefix} [#{timestamp}] #{message}"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def log_progress(current, total, message = nil)
|
|
90
|
+
percentage = ((current.to_f / total) * 100).round(2)
|
|
91
|
+
msg = message ? "#{message} (#{current}/#{total} - #{percentage}%)" : "Progress: #{current}/#{total} (#{percentage}%)"
|
|
92
|
+
log(msg)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def process_in_batches(relation, batch_size: 1000, &block)
|
|
96
|
+
total = relation.count
|
|
97
|
+
log("There are #{total} records to process")
|
|
98
|
+
return 0 if total.zero?
|
|
99
|
+
|
|
100
|
+
processed = 0
|
|
101
|
+
log("Processing #{total} records in batches of #{batch_size}")
|
|
102
|
+
relation.find_each(batch_size: batch_size) do |record|
|
|
103
|
+
block.call(record)
|
|
104
|
+
processed += 1
|
|
105
|
+
log_interval = [batch_size, (total * 0.1).to_i].max
|
|
106
|
+
log_progress(processed, total) if (processed % log_interval).zero?
|
|
107
|
+
end
|
|
108
|
+
log_progress(processed, total, 'Completed')
|
|
109
|
+
processed
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ScriptTracker
|
|
4
|
+
class ExecutedScript < ActiveRecord::Base
|
|
5
|
+
self.table_name = 'executed_scripts'
|
|
6
|
+
|
|
7
|
+
# Constants
|
|
8
|
+
DEFAULT_TIMEOUT = 300 # 5 minutes in seconds
|
|
9
|
+
|
|
10
|
+
# Validations
|
|
11
|
+
validates :filename, presence: true, uniqueness: true
|
|
12
|
+
validates :executed_at, presence: true
|
|
13
|
+
validates :status, presence: true, inclusion: { in: %w[success failed running skipped] }
|
|
14
|
+
validates :timeout, numericality: { greater_than: 0, allow_nil: true }
|
|
15
|
+
|
|
16
|
+
# Scopes
|
|
17
|
+
scope :successful, -> { where(status: 'success') }
|
|
18
|
+
scope :failed, -> { where(status: 'failed') }
|
|
19
|
+
scope :running, -> { where(status: 'running') }
|
|
20
|
+
scope :completed, -> { where(status: %w[success failed]) }
|
|
21
|
+
scope :skipped, -> { where(status: 'skipped') }
|
|
22
|
+
scope :ordered, -> { order(executed_at: :asc) }
|
|
23
|
+
scope :recent_first, -> { order(executed_at: :desc) }
|
|
24
|
+
|
|
25
|
+
# Class methods
|
|
26
|
+
def self.executed?(filename)
|
|
27
|
+
exists?(filename: filename)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.mark_as_running(filename)
|
|
31
|
+
create!(
|
|
32
|
+
filename: filename,
|
|
33
|
+
executed_at: Time.current,
|
|
34
|
+
status: 'running'
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.cleanup_stale_running_scripts(older_than: 1.hour.ago)
|
|
39
|
+
stale_scripts = running.where('executed_at < ?', older_than)
|
|
40
|
+
count = stale_scripts.count
|
|
41
|
+
stale_scripts.update_all(
|
|
42
|
+
status: 'failed',
|
|
43
|
+
output: 'Script was marked as failed due to stale running status'
|
|
44
|
+
)
|
|
45
|
+
count
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Instance methods
|
|
49
|
+
def mark_success!(output_text = nil, execution_duration = nil)
|
|
50
|
+
update!(
|
|
51
|
+
status: 'success',
|
|
52
|
+
output: output_text,
|
|
53
|
+
duration: execution_duration
|
|
54
|
+
)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def mark_failed!(error_message, execution_duration = nil)
|
|
58
|
+
update!(
|
|
59
|
+
status: 'failed',
|
|
60
|
+
output: error_message,
|
|
61
|
+
duration: execution_duration
|
|
62
|
+
)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def mark_skipped!(output_text = nil, execution_duration = nil)
|
|
66
|
+
update!(
|
|
67
|
+
status: 'skipped',
|
|
68
|
+
output: output_text,
|
|
69
|
+
duration: execution_duration
|
|
70
|
+
)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def success?
|
|
74
|
+
status == 'success'
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def failed?
|
|
78
|
+
status == 'failed'
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def running?
|
|
82
|
+
status == 'running'
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def skipped?
|
|
86
|
+
status == 'skipped'
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def formatted_duration
|
|
90
|
+
return 'N/A' if duration.nil?
|
|
91
|
+
|
|
92
|
+
if duration < 1
|
|
93
|
+
"#{(duration * 1000).round(2)}ms"
|
|
94
|
+
elsif duration < 60
|
|
95
|
+
"#{duration.round(2)}s"
|
|
96
|
+
else
|
|
97
|
+
minutes = (duration / 60).floor
|
|
98
|
+
seconds = (duration % 60).round(2)
|
|
99
|
+
"#{minutes}m #{seconds}s"
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def formatted_output
|
|
104
|
+
return 'No output' if output.blank?
|
|
105
|
+
|
|
106
|
+
output.truncate(500)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def timeout_seconds
|
|
110
|
+
timeout || DEFAULT_TIMEOUT
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def timed_out?
|
|
114
|
+
return false unless running? && timeout
|
|
115
|
+
|
|
116
|
+
Time.current > executed_at + timeout.seconds
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails/generators'
|
|
4
|
+
require 'rails/generators/migration'
|
|
5
|
+
require 'rails/generators/active_record'
|
|
6
|
+
|
|
7
|
+
module ScriptTracker
|
|
8
|
+
module Generators
|
|
9
|
+
class InstallGenerator < Rails::Generators::Base
|
|
10
|
+
include Rails::Generators::Migration
|
|
11
|
+
|
|
12
|
+
source_root File.expand_path('templates', __dir__)
|
|
13
|
+
desc "Creates ScriptTracker migration file and initializer"
|
|
14
|
+
|
|
15
|
+
class_option :uuid, type: :boolean, default: true,
|
|
16
|
+
desc: "Use UUID for primary keys (requires database support)"
|
|
17
|
+
|
|
18
|
+
class_option :skip_migration, type: :boolean, default: false,
|
|
19
|
+
desc: "Skip creating the migration file"
|
|
20
|
+
|
|
21
|
+
class_option :skip_initializer, type: :boolean, default: false,
|
|
22
|
+
desc: "Skip creating the initializer file"
|
|
23
|
+
|
|
24
|
+
def self.next_migration_number(dirname)
|
|
25
|
+
next_migration_number = current_migration_number(dirname) + 1
|
|
26
|
+
ActiveRecord::Migration.next_migration_number(next_migration_number)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def copy_migration
|
|
30
|
+
return if options[:skip_migration]
|
|
31
|
+
|
|
32
|
+
migration_template(
|
|
33
|
+
"create_executed_scripts.rb.erb",
|
|
34
|
+
"db/migrate/create_executed_scripts.rb",
|
|
35
|
+
migration_version: migration_version
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def create_initializer
|
|
40
|
+
return if options[:skip_initializer]
|
|
41
|
+
|
|
42
|
+
template "initializer.rb", "config/initializers/script_tracker.rb"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def create_scripts_directory
|
|
46
|
+
empty_directory "lib/scripts"
|
|
47
|
+
create_file "lib/scripts/.keep"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def show_readme
|
|
51
|
+
readme "README" if behavior == :invoke
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def migration_version
|
|
57
|
+
"[#{ActiveRecord::Migration.current_version}]"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def use_uuid?
|
|
61
|
+
options[:uuid]
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def primary_key_type
|
|
65
|
+
use_uuid? ? ":uuid" : "true"
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
===============================================================================
|
|
2
|
+
|
|
3
|
+
ScriptTracker has been installed!
|
|
4
|
+
|
|
5
|
+
Next steps:
|
|
6
|
+
|
|
7
|
+
1. Run the migration:
|
|
8
|
+
rails db:migrate
|
|
9
|
+
|
|
10
|
+
2. Create your first script:
|
|
11
|
+
rake scripts:create["your script description"]
|
|
12
|
+
|
|
13
|
+
3. Run pending scripts:
|
|
14
|
+
rake scripts:run
|
|
15
|
+
|
|
16
|
+
4. Check scripts status:
|
|
17
|
+
rake scripts:status
|
|
18
|
+
|
|
19
|
+
For more information, visit:
|
|
20
|
+
https://github.com/blink-global/script_tracker
|
|
21
|
+
|
|
22
|
+
===============================================================================
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
class CreateExecutedScripts < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
|
|
2
|
+
def change
|
|
3
|
+
create_table :executed_scripts<%= primary_key_type == ":uuid" ? ", id: :uuid" : "" %> do |t|
|
|
4
|
+
t.string :filename, null: false
|
|
5
|
+
t.datetime :executed_at, null: false
|
|
6
|
+
t.string :status, null: false, default: 'running'
|
|
7
|
+
t.text :output
|
|
8
|
+
t.float :duration
|
|
9
|
+
t.integer :timeout
|
|
10
|
+
t.timestamps
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
add_index :executed_scripts, :filename, unique: true
|
|
14
|
+
add_index :executed_scripts, :status
|
|
15
|
+
add_index :executed_scripts, :executed_at
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ScriptTracker
|
|
4
|
+
class Railtie < ::Rails::Railtie
|
|
5
|
+
railtie_name :script_tracker
|
|
6
|
+
|
|
7
|
+
rake_tasks do
|
|
8
|
+
path = File.expand_path(__dir__)
|
|
9
|
+
Dir.glob("#{path}/../../tasks/**/*.rake").each { |f| load f }
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
initializer "script_tracker.configure" do |app|
|
|
13
|
+
app.config.script_tracker = ActiveSupport::OrderedOptions.new
|
|
14
|
+
app.config.script_tracker.scripts_path = app.root.join('lib', 'scripts')
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
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
|
+
|
|
8
|
+
# Load generators for Rails
|
|
9
|
+
if defined?(Rails)
|
|
10
|
+
require "rails/generators"
|
|
11
|
+
require_relative "script_tracker/generators/install_generator"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
module ScriptTracker
|
|
15
|
+
class Error < StandardError; end
|
|
16
|
+
|
|
17
|
+
class << self
|
|
18
|
+
attr_accessor :configuration
|
|
19
|
+
|
|
20
|
+
def configure
|
|
21
|
+
self.configuration ||= Configuration.new
|
|
22
|
+
yield(configuration) if block_given?
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def scripts_path
|
|
26
|
+
configuration&.scripts_path || Rails.root.join('lib', 'scripts')
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
class Configuration
|
|
31
|
+
attr_accessor :scripts_path
|
|
32
|
+
|
|
33
|
+
def initialize
|
|
34
|
+
@scripts_path = nil
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
namespace :scripts do
|
|
4
|
+
desc 'Create a new script'
|
|
5
|
+
task :create, [:description] => :environment do |_task, args|
|
|
6
|
+
description = args[:description]
|
|
7
|
+
|
|
8
|
+
if description.blank?
|
|
9
|
+
puts 'Usage: rake scripts:create["description"]'
|
|
10
|
+
exit 1
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
timestamp = Time.current.strftime('%Y%m%d%H%M%S')
|
|
14
|
+
snake_case = description.downcase.gsub(/[^a-z0-9]+/, '_').gsub(/^_|_$/, '')
|
|
15
|
+
filename = "#{timestamp}_#{snake_case}.rb"
|
|
16
|
+
class_name = snake_case.camelize
|
|
17
|
+
scripts_dir = ScriptTracker.scripts_path
|
|
18
|
+
file_path = scripts_dir.join(filename)
|
|
19
|
+
|
|
20
|
+
FileUtils.mkdir_p(scripts_dir) unless Dir.exist?(scripts_dir)
|
|
21
|
+
|
|
22
|
+
template = File.read(File.expand_path('../templates/script_template.rb', __dir__))
|
|
23
|
+
content = template.gsub('<%= filename %>', filename)
|
|
24
|
+
.gsub('<%= description %>', description)
|
|
25
|
+
.gsub('<%= class_name %>', class_name)
|
|
26
|
+
.gsub('<%= Time.current.year %>', Time.current.year.to_s)
|
|
27
|
+
.gsub('<%= Time.current.strftime(\'%Y-%m-%d %H:%M:%S\') %>', Time.current.strftime('%Y-%m-%d %H:%M:%S'))
|
|
28
|
+
|
|
29
|
+
File.write(file_path, content)
|
|
30
|
+
|
|
31
|
+
puts "\nCreated #{filename}"
|
|
32
|
+
puts "Location: #{file_path}"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
desc 'Run all pending scripts'
|
|
36
|
+
task run: :environment do
|
|
37
|
+
# Clean up any scripts that got stuck in "running" state
|
|
38
|
+
stale_count = ScriptTracker::ExecutedScript.cleanup_stale_running_scripts
|
|
39
|
+
puts "Cleaned up #{stale_count} stale script(s)\n\n" if stale_count > 0
|
|
40
|
+
|
|
41
|
+
# Find all scripts that haven't run yet
|
|
42
|
+
scripts_dir = ScriptTracker.scripts_path
|
|
43
|
+
|
|
44
|
+
unless Dir.exist?(scripts_dir)
|
|
45
|
+
puts "Error: Scripts directory does not exist: #{scripts_dir}"
|
|
46
|
+
puts "Please run: rails generate script_tracker:install"
|
|
47
|
+
exit 1
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
all_scripts = Dir[scripts_dir.join('*.rb')].sort
|
|
51
|
+
pending_scripts = all_scripts.reject { |f| ScriptTracker::ExecutedScript.executed?(File.basename(f)) }
|
|
52
|
+
|
|
53
|
+
if pending_scripts.empty?
|
|
54
|
+
puts "\nAll scripts are up to date. Nothing to run!"
|
|
55
|
+
exit 0
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
success_count = 0
|
|
59
|
+
failed_count = 0
|
|
60
|
+
skipped_count = 0
|
|
61
|
+
|
|
62
|
+
pending_scripts.each_with_index do |script_path, index|
|
|
63
|
+
script_name = File.basename(script_path)
|
|
64
|
+
puts "[#{index + 1}/#{pending_scripts.count}] Running #{script_name}..."
|
|
65
|
+
|
|
66
|
+
begin
|
|
67
|
+
# Load script file
|
|
68
|
+
load script_path
|
|
69
|
+
|
|
70
|
+
# Extract and validate class name
|
|
71
|
+
class_name = script_name.gsub(/^\d+_/, '').gsub('.rb', '').camelize
|
|
72
|
+
script_class_name = "Scripts::#{class_name}"
|
|
73
|
+
|
|
74
|
+
unless defined?(Scripts)
|
|
75
|
+
puts "Error: Scripts module not found. Ensure script defines 'module Scripts'"
|
|
76
|
+
failed_count += 1
|
|
77
|
+
next
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
begin
|
|
81
|
+
script_class = script_class_name.constantize
|
|
82
|
+
rescue NameError => e
|
|
83
|
+
puts "Error: Could not find class #{script_class_name}: #{e.message}"
|
|
84
|
+
failed_count += 1
|
|
85
|
+
next
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Validate script class
|
|
89
|
+
unless script_class < ScriptTracker::Base
|
|
90
|
+
puts "Error: Script class #{script_class_name} must inherit from ScriptTracker::Base"
|
|
91
|
+
failed_count += 1
|
|
92
|
+
next
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
unless script_class.respond_to?(:execute)
|
|
96
|
+
puts "Error: Script class #{script_class_name} must implement the execute method"
|
|
97
|
+
failed_count += 1
|
|
98
|
+
next
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Mark as running and get timeout
|
|
102
|
+
executed_script = ScriptTracker::ExecutedScript.mark_as_running(script_name)
|
|
103
|
+
executed_script.update(timeout: script_class.timeout) if script_class.respond_to?(:timeout)
|
|
104
|
+
start_time = Time.current
|
|
105
|
+
|
|
106
|
+
# Run script with timeout support
|
|
107
|
+
result = script_class.run(executed_script)
|
|
108
|
+
duration = Time.current - start_time
|
|
109
|
+
|
|
110
|
+
if result[:success]
|
|
111
|
+
executed_script.mark_success!(result[:output], result[:duration] || duration)
|
|
112
|
+
puts "Completed successfully in #{duration.round(2)}s\n\n"
|
|
113
|
+
success_count += 1
|
|
114
|
+
elsif result[:skipped]
|
|
115
|
+
executed_script.mark_skipped!(result[:output], result[:duration] || duration)
|
|
116
|
+
puts "Skipped in #{duration.round(2)}s\n\n"
|
|
117
|
+
skipped_count += 1
|
|
118
|
+
else
|
|
119
|
+
executed_script.mark_failed!(result[:output], result[:duration] || duration)
|
|
120
|
+
puts "Failed in #{duration.round(2)}s"
|
|
121
|
+
puts "Error: #{result[:output]}\n\n"
|
|
122
|
+
failed_count += 1
|
|
123
|
+
end
|
|
124
|
+
rescue LoadError => e
|
|
125
|
+
puts "Error: Could not load script file #{script_name}: #{e.message}"
|
|
126
|
+
failed_count += 1
|
|
127
|
+
rescue SyntaxError => e
|
|
128
|
+
puts "Error: Syntax error in script #{script_name}: #{e.message}"
|
|
129
|
+
failed_count += 1
|
|
130
|
+
rescue StandardError => e
|
|
131
|
+
duration = Time.current - start_time rescue 0
|
|
132
|
+
error_message = "#{e.class}: #{e.message}"
|
|
133
|
+
executed_script&.mark_failed!(error_message, duration)
|
|
134
|
+
puts "Error: #{e.message}\n\n"
|
|
135
|
+
failed_count += 1
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
puts "Summary: #{success_count} succeeded, #{failed_count} failed, #{skipped_count} skipped"
|
|
140
|
+
|
|
141
|
+
exit(1) if failed_count > 0
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
desc 'Show all scripts status'
|
|
145
|
+
task status: :environment do
|
|
146
|
+
scripts_dir = ScriptTracker.scripts_path
|
|
147
|
+
|
|
148
|
+
unless Dir.exist?(scripts_dir)
|
|
149
|
+
puts "Error: Scripts directory does not exist: #{scripts_dir}"
|
|
150
|
+
puts "Please run: rails generate script_tracker:install"
|
|
151
|
+
exit 1
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
script_files = Dir[scripts_dir.join('*.rb')].sort
|
|
155
|
+
executed_scripts = ScriptTracker::ExecutedScript.all.index_by(&:filename)
|
|
156
|
+
|
|
157
|
+
if script_files.empty?
|
|
158
|
+
puts "\nNo scripts found in #{scripts_dir}"
|
|
159
|
+
puts "Create a script with: rake scripts:create[\"description\"]"
|
|
160
|
+
exit 0
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
puts "\nScripts:"
|
|
164
|
+
script_files.each do |file|
|
|
165
|
+
filename = File.basename(file)
|
|
166
|
+
if (script = executed_scripts[filename])
|
|
167
|
+
status_icon = script.success? ? '[SUCCESS]' : script.failed? ? '[FAILED]' : script.skipped? ? '[SKIPPED]' : '[RUNNING]'
|
|
168
|
+
puts " #{status_icon} #{filename} (#{script.formatted_duration})"
|
|
169
|
+
else
|
|
170
|
+
puts " [PENDING] #{filename}"
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
puts
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
desc 'Rollback a script'
|
|
177
|
+
task :rollback, [:filename] => :environment do |_task, args|
|
|
178
|
+
filename = args[:filename]
|
|
179
|
+
|
|
180
|
+
if filename.blank?
|
|
181
|
+
puts 'Usage: rake scripts:rollback[filename.rb]'
|
|
182
|
+
exit 1
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
script = ScriptTracker::ExecutedScript.find_by(filename: filename)
|
|
186
|
+
if script.nil?
|
|
187
|
+
puts "Script not found: #{filename}"
|
|
188
|
+
exit 1
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
script.destroy!
|
|
192
|
+
puts "Rolled back: #{filename}"
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
desc 'Cleanup stale scripts'
|
|
196
|
+
task cleanup: :environment do
|
|
197
|
+
count = ScriptTracker::ExecutedScript.cleanup_stale_running_scripts
|
|
198
|
+
puts "Cleaned up #{count} stale script(s)"
|
|
199
|
+
end
|
|
200
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Script: <%= filename %>
|
|
4
|
+
# Created: <%= Time.current.strftime('%Y-%m-%d %H:%M:%S') %>
|
|
5
|
+
# Description: <%= description %>
|
|
6
|
+
#
|
|
7
|
+
# This script performs: [Describe what this script does]
|
|
8
|
+
#
|
|
9
|
+
# Prerequisites:
|
|
10
|
+
# - [List any prerequisites]
|
|
11
|
+
#
|
|
12
|
+
# Rollback plan:
|
|
13
|
+
# - [Describe how to rollback if needed]
|
|
14
|
+
#
|
|
15
|
+
|
|
16
|
+
module Scripts
|
|
17
|
+
class <%= class_name %> < ScriptTracker::Base
|
|
18
|
+
# Optional: Override timeout (default is 300 seconds / 5 minutes)
|
|
19
|
+
# def self.timeout
|
|
20
|
+
# 3600 # 1 hour
|
|
21
|
+
# end
|
|
22
|
+
|
|
23
|
+
def self.execute
|
|
24
|
+
log "Starting script: <%= filename %>"
|
|
25
|
+
|
|
26
|
+
# Example: Skip if work is already done
|
|
27
|
+
# if condition_already_met?
|
|
28
|
+
# skip! "Reason for skipping"
|
|
29
|
+
# end
|
|
30
|
+
|
|
31
|
+
# Your script logic here
|
|
32
|
+
|
|
33
|
+
log "Script completed successfully"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Execute the script if run directly
|
|
39
|
+
if __FILE__ == $PROGRAM_NAME
|
|
40
|
+
result = Scripts::<%= class_name %>.run
|
|
41
|
+
exit(result[:success] ? 0 : 1)
|
|
42
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: script_tracker
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Ahmed Abd El-Latif
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2025-11-17 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: activerecord
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '6.0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '6.0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: activesupport
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - ">="
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '6.0'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - ">="
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '6.0'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: database_cleaner-active_record
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '2.0'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '2.0'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: rake
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - "~>"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '13.0'
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '13.0'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: rspec
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - "~>"
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '3.0'
|
|
76
|
+
type: :development
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - "~>"
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '3.0'
|
|
83
|
+
- !ruby/object:Gem::Dependency
|
|
84
|
+
name: sqlite3
|
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - "~>"
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: '2.0'
|
|
90
|
+
type: :development
|
|
91
|
+
prerelease: false
|
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - "~>"
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: '2.0'
|
|
97
|
+
description: ScriptTracker provides a migration-like system for managing one-off scripts
|
|
98
|
+
with execution tracking, transaction support, and built-in logging. Perfect for
|
|
99
|
+
data migrations, cleanup tasks, and administrative scripts.
|
|
100
|
+
email:
|
|
101
|
+
- ahmed.abdelatif@blink.com
|
|
102
|
+
executables: []
|
|
103
|
+
extensions: []
|
|
104
|
+
extra_rdoc_files: []
|
|
105
|
+
files:
|
|
106
|
+
- CHANGELOG.md
|
|
107
|
+
- LICENSE
|
|
108
|
+
- README.md
|
|
109
|
+
- lib/script_tracker.rb
|
|
110
|
+
- lib/script_tracker/base.rb
|
|
111
|
+
- lib/script_tracker/executed_script.rb
|
|
112
|
+
- lib/script_tracker/generators/install_generator.rb
|
|
113
|
+
- lib/script_tracker/generators/templates/README
|
|
114
|
+
- lib/script_tracker/generators/templates/create_executed_scripts.rb.erb
|
|
115
|
+
- lib/script_tracker/generators/templates/initializer.rb
|
|
116
|
+
- lib/script_tracker/railtie.rb
|
|
117
|
+
- lib/script_tracker/version.rb
|
|
118
|
+
- tasks/script_tracker.rake
|
|
119
|
+
- templates/script_template.rb
|
|
120
|
+
homepage: https://github.com/blink-global/script_tracker
|
|
121
|
+
licenses:
|
|
122
|
+
- MIT
|
|
123
|
+
metadata:
|
|
124
|
+
homepage_uri: https://github.com/blink-global/script_tracker
|
|
125
|
+
source_code_uri: https://github.com/a-abdellatif98/script_tracker
|
|
126
|
+
changelog_uri: https://github.com/a-abdellatif98/script_tracker/blob/main/CHANGELOG.md
|
|
127
|
+
post_install_message:
|
|
128
|
+
rdoc_options: []
|
|
129
|
+
require_paths:
|
|
130
|
+
- lib
|
|
131
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
132
|
+
requirements:
|
|
133
|
+
- - ">="
|
|
134
|
+
- !ruby/object:Gem::Version
|
|
135
|
+
version: 2.7.0
|
|
136
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
137
|
+
requirements:
|
|
138
|
+
- - ">="
|
|
139
|
+
- !ruby/object:Gem::Version
|
|
140
|
+
version: '0'
|
|
141
|
+
requirements: []
|
|
142
|
+
rubygems_version: 3.4.1
|
|
143
|
+
signing_key:
|
|
144
|
+
specification_version: 4
|
|
145
|
+
summary: A Ruby gem for tracking and managing one-off script executions in Rails applications
|
|
146
|
+
test_files: []
|