web_rake 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/MIT-LICENSE +22 -0
- data/README.md +117 -0
- data/Rakefile +6 -0
- data/app/controllers/web_rake/application_controller.rb +15 -0
- data/app/controllers/web_rake/tasks_controller.rb +152 -0
- data/app/views/layouts/web_rake/application.html.erb +95 -0
- data/app/views/web_rake/tasks/execute.html.erb +193 -0
- data/app/views/web_rake/tasks/index.html.erb +109 -0
- data/config/routes.rb +6 -0
- data/lib/web_rake/engine.rb +16 -0
- data/lib/web_rake/version.rb +3 -0
- data/lib/web_rake.rb +11 -0
- metadata +98 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: e01fea2418b7eaccb61e4a536870df1ffd7d21c6637da249f1489e89b3c82d8d
|
|
4
|
+
data.tar.gz: b355c7545ac8027223d4e1fcb2c4c63a02eb6a13ebc3b4f4dcbbc2219622a70a
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 7e842bd3432fb67758c398543d8858ab3094e52ca8ddae73ee24681b90e7d9900d2c61c227e2d465a9149999a9b4a467841e3dd24d6b29380858d9f884c3d4d2
|
|
7
|
+
data.tar.gz: 32712a2c211e7bcdc280b112be8a048feb9d6033bb06a5704f0ec0483719c38e57a5a28128a5e7f662bd60f89d5f3891b1961e867353741b6c56ac739fca9637
|
data/MIT-LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Copyright (c) 2024 Your Name
|
|
2
|
+
|
|
3
|
+
MIT License
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
6
|
+
a copy of this software and associated documentation files (the
|
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
11
|
+
the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be
|
|
14
|
+
included in all copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# WebRake
|
|
2
|
+
|
|
3
|
+
WebRake is a Rails engine designed for production environments that provides a secure web interface for executing essential Rake tasks in your deployed Rails application. It includes HTTP basic authentication for security and automatically mounts at `/rails/tasks` without requiring any route configuration.
|
|
4
|
+
|
|
5
|
+
## Purpose
|
|
6
|
+
|
|
7
|
+
This gem is specifically designed for production/deployed applications where you need quick access to:
|
|
8
|
+
- Seed your database with `db:seed`
|
|
9
|
+
- Run custom data population tasks (like `sample_data`)
|
|
10
|
+
- Execute other custom maintenance tasks
|
|
11
|
+
|
|
12
|
+
Perfect for situations where you don't have shell access or need to quickly run tasks on deployed platforms like Render, Heroku, or similar services.
|
|
13
|
+
|
|
14
|
+
## Features
|
|
15
|
+
|
|
16
|
+
- Exposes `db:seed` and custom Rake tasks from `lib/tasks/` directory
|
|
17
|
+
- Web interface with one-click task execution
|
|
18
|
+
- HTTP basic authentication for security
|
|
19
|
+
- Real-time output capture (stdout and stderr)
|
|
20
|
+
- Execution timing and status reporting
|
|
21
|
+
- Clean, responsive UI
|
|
22
|
+
- Zero configuration required for routes
|
|
23
|
+
|
|
24
|
+
## Which Tasks Are Shown
|
|
25
|
+
|
|
26
|
+
WebRake displays:
|
|
27
|
+
- The `db:seed` task for database seeding
|
|
28
|
+
- Custom Rake tasks defined in your `lib/tasks/` directory
|
|
29
|
+
|
|
30
|
+
System tasks (like `db:migrate`, `db:drop`, `assets:precompile`, etc.) are intentionally hidden for security reasons.
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
Add this line to your application's Gemfile:
|
|
35
|
+
|
|
36
|
+
```ruby
|
|
37
|
+
gem 'web_rake'
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
And then execute:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
bundle install
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Or install it yourself as:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
gem install web_rake
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Configuration
|
|
53
|
+
|
|
54
|
+
### Environment Variables
|
|
55
|
+
|
|
56
|
+
WebRake uses HTTP basic authentication to protect access to the rake task interface. Set these environment variables in your `.env` file or deployment configuration:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
WEB_RAKE_USERNAME=your_username
|
|
60
|
+
WEB_RAKE_PASSWORD=your_secure_password
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**Important:** Choose strong credentials, especially in production environments, as anyone with these credentials can execute rake tasks on your application.
|
|
64
|
+
|
|
65
|
+
### Optional Ruby Configuration
|
|
66
|
+
|
|
67
|
+
You can also configure the credentials programmatically in an initializer:
|
|
68
|
+
|
|
69
|
+
```ruby
|
|
70
|
+
# config/initializers/web_rake.rb
|
|
71
|
+
WebRake.configure do |config|
|
|
72
|
+
config.username = 'custom_username'
|
|
73
|
+
config.password = 'custom_password'
|
|
74
|
+
end
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Note: Environment variables take precedence over Ruby configuration.
|
|
78
|
+
|
|
79
|
+
## Usage
|
|
80
|
+
|
|
81
|
+
Once installed and configured, WebRake automatically mounts at `/rails/tasks` in your Rails application.
|
|
82
|
+
|
|
83
|
+
1. Navigate to `http://your-app.com/rails/tasks`
|
|
84
|
+
2. Enter your HTTP basic auth credentials
|
|
85
|
+
3. You'll see a list of all available Rake tasks with descriptions
|
|
86
|
+
4. Click "Run Task" on any task to execute it
|
|
87
|
+
5. View the output, errors (if any), and execution time
|
|
88
|
+
|
|
89
|
+
## How It Works
|
|
90
|
+
|
|
91
|
+
- WebRake is a Rails engine that automatically mounts itself at `/rails/tasks`
|
|
92
|
+
- It discovers all loaded Rake tasks in your Rails application
|
|
93
|
+
- Tasks are executed in the same process as your Rails app
|
|
94
|
+
- Output is captured using StringIO redirection
|
|
95
|
+
- Tasks are automatically re-enabled after execution for repeat runs
|
|
96
|
+
|
|
97
|
+
## Development
|
|
98
|
+
|
|
99
|
+
After checking out the repo, run:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
bundle install
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
To run tests:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
bundle exec rspec
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Contributing
|
|
112
|
+
|
|
113
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/firstdraft/web_rake.
|
|
114
|
+
|
|
115
|
+
## License
|
|
116
|
+
|
|
117
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module WebRake
|
|
2
|
+
class ApplicationController < ActionController::Base
|
|
3
|
+
protect_from_forgery with: :exception
|
|
4
|
+
|
|
5
|
+
before_action :authenticate
|
|
6
|
+
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def authenticate
|
|
10
|
+
authenticate_or_request_with_http_basic do |username, password|
|
|
11
|
+
username == WebRake.username && password == WebRake.password
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
module WebRake
|
|
2
|
+
class TasksController < ApplicationController
|
|
3
|
+
before_action :ensure_environment_loaded
|
|
4
|
+
|
|
5
|
+
def index
|
|
6
|
+
@tasks = custom_tasks.sort_by(&:name)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def show
|
|
10
|
+
@task = find_task
|
|
11
|
+
redirect_to root_path, alert: "Task not found" unless @task
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def execute
|
|
15
|
+
task_name = params[:id].gsub('__', ':')
|
|
16
|
+
|
|
17
|
+
# Check if this is one of our allowed tasks (custom tasks + db:seed)
|
|
18
|
+
allowed_tasks = extract_task_names_from_files + ['db:seed']
|
|
19
|
+
unless allowed_tasks.include?(task_name)
|
|
20
|
+
redirect_to root_path, alert: "Task not found"
|
|
21
|
+
return
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
start_time = Time.current
|
|
25
|
+
output = []
|
|
26
|
+
errors = []
|
|
27
|
+
success = false
|
|
28
|
+
|
|
29
|
+
begin
|
|
30
|
+
original_stdout = $stdout
|
|
31
|
+
original_stderr = $stderr
|
|
32
|
+
|
|
33
|
+
$stdout = StringIO.new
|
|
34
|
+
$stderr = StringIO.new
|
|
35
|
+
|
|
36
|
+
# Run the rake task in a subprocess to avoid state issues
|
|
37
|
+
# This ensures clean execution every time
|
|
38
|
+
require 'open3'
|
|
39
|
+
|
|
40
|
+
# Build the rake command
|
|
41
|
+
rake_command = "bundle exec rake #{task_name}"
|
|
42
|
+
|
|
43
|
+
# Execute the command and capture output
|
|
44
|
+
stdout_str, stderr_str, status = Open3.capture3(rake_command)
|
|
45
|
+
|
|
46
|
+
output = stdout_str.split("\n")
|
|
47
|
+
errors = stderr_str.split("\n") if stderr_str.present?
|
|
48
|
+
success = status.success?
|
|
49
|
+
rescue => e
|
|
50
|
+
errors << e.message
|
|
51
|
+
errors.concat(e.backtrace) if Rails.env.development?
|
|
52
|
+
ensure
|
|
53
|
+
$stdout = original_stdout if original_stdout
|
|
54
|
+
$stderr = original_stderr if original_stderr
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
end_time = Time.current
|
|
58
|
+
duration = (end_time - start_time).round(2)
|
|
59
|
+
|
|
60
|
+
# Create a simple task object for the view
|
|
61
|
+
task = OpenStruct.new(name: task_name)
|
|
62
|
+
|
|
63
|
+
render :execute, locals: {
|
|
64
|
+
task: task,
|
|
65
|
+
output: output,
|
|
66
|
+
errors: errors,
|
|
67
|
+
success: success,
|
|
68
|
+
duration: duration
|
|
69
|
+
}
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def ensure_environment_loaded
|
|
75
|
+
# Ensure Rails environment is available for tasks that depend on it
|
|
76
|
+
Rails.application.load_tasks if Rake::Task.tasks.empty?
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def custom_tasks
|
|
80
|
+
# First, get the names of tasks defined in lib/tasks files
|
|
81
|
+
custom_task_names = extract_task_names_from_files
|
|
82
|
+
|
|
83
|
+
# Always include db:seed task
|
|
84
|
+
custom_task_names << 'db:seed'
|
|
85
|
+
|
|
86
|
+
# Ensure all tasks are loaded
|
|
87
|
+
Rails.application.load_tasks
|
|
88
|
+
|
|
89
|
+
# Get only the tasks that match our custom task names
|
|
90
|
+
custom_task_names.filter_map do |name|
|
|
91
|
+
Rake::Task[name] rescue nil
|
|
92
|
+
end.select(&:actions)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def extract_task_names_from_files
|
|
96
|
+
task_names = []
|
|
97
|
+
|
|
98
|
+
# Parse each rake file to extract task names
|
|
99
|
+
Dir.glob(Rails.root.join('lib/tasks/**/*.rake')).each do |file|
|
|
100
|
+
content = File.read(file)
|
|
101
|
+
|
|
102
|
+
# Match various task definition patterns
|
|
103
|
+
|
|
104
|
+
# Pattern for: task({ :sample_data => :environment })
|
|
105
|
+
content.scan(/task\s*\(\s*\{\s*:([a-zA-Z_][a-zA-Z0-9_]*)\s*=>/).each do |match|
|
|
106
|
+
task_names << match[0]
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Pattern for: task :sample_data => :environment
|
|
110
|
+
content.scan(/task\s+:([a-zA-Z_][a-zA-Z0-9_]*)\s*=>/).each do |match|
|
|
111
|
+
task_names << match[0]
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Pattern for: task "sample_data" => :environment
|
|
115
|
+
content.scan(/task\s+["']([a-zA-Z_][a-zA-Z0-9_]*)["']\s*=>/).each do |match|
|
|
116
|
+
task_names << match[0]
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Pattern for: task sample_data: :environment (Ruby 1.9+ syntax)
|
|
120
|
+
content.scan(/task\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*:/).each do |match|
|
|
121
|
+
task_names << match[0]
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Pattern for: task(:sample_data)
|
|
125
|
+
content.scan(/task\s*\(\s*:([a-zA-Z_][a-zA-Z0-9_]*)\s*\)/).each do |match|
|
|
126
|
+
task_names << match[0]
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Pattern for: desc "..." \n task :name
|
|
130
|
+
content.scan(/desc\s+.*?\n\s*task\s+:([a-zA-Z_][a-zA-Z0-9_]*)/).each do |match|
|
|
131
|
+
task_names << match[0]
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
task_names.uniq
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def find_task
|
|
139
|
+
task_name = params[:id].gsub('__', ':')
|
|
140
|
+
|
|
141
|
+
# Check if this is one of our allowed tasks (custom tasks + db:seed)
|
|
142
|
+
allowed_tasks = extract_task_names_from_files + ['db:seed']
|
|
143
|
+
return nil unless allowed_tasks.include?(task_name)
|
|
144
|
+
|
|
145
|
+
# Ensure tasks are loaded
|
|
146
|
+
Rails.application.load_tasks
|
|
147
|
+
|
|
148
|
+
# Find the task
|
|
149
|
+
Rake::Task[task_name] rescue nil
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<title>Web Rake</title>
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
|
+
<%= csrf_meta_tags %>
|
|
7
|
+
<%= csp_meta_tag %>
|
|
8
|
+
|
|
9
|
+
<style>
|
|
10
|
+
* {
|
|
11
|
+
margin: 0;
|
|
12
|
+
padding: 0;
|
|
13
|
+
box-sizing: border-box;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
body {
|
|
17
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
18
|
+
line-height: 1.6;
|
|
19
|
+
color: #333;
|
|
20
|
+
background-color: #f5f5f5;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.container {
|
|
24
|
+
max-width: 1200px;
|
|
25
|
+
margin: 0 auto;
|
|
26
|
+
padding: 20px;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.header {
|
|
30
|
+
background-color: #2c3e50;
|
|
31
|
+
color: white;
|
|
32
|
+
padding: 1rem 0;
|
|
33
|
+
margin-bottom: 2rem;
|
|
34
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.header h1 {
|
|
38
|
+
margin: 0;
|
|
39
|
+
font-size: 1.8rem;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.header a {
|
|
43
|
+
color: white;
|
|
44
|
+
text-decoration: none;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.alert {
|
|
48
|
+
padding: 12px 20px;
|
|
49
|
+
margin-bottom: 20px;
|
|
50
|
+
border-radius: 4px;
|
|
51
|
+
background-color: #f8d7da;
|
|
52
|
+
color: #721c24;
|
|
53
|
+
border: 1px solid #f5c6cb;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.notice {
|
|
57
|
+
padding: 12px 20px;
|
|
58
|
+
margin-bottom: 20px;
|
|
59
|
+
border-radius: 4px;
|
|
60
|
+
background-color: #d4edda;
|
|
61
|
+
color: #155724;
|
|
62
|
+
border: 1px solid #c3e6cb;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.content {
|
|
66
|
+
background-color: white;
|
|
67
|
+
padding: 30px;
|
|
68
|
+
border-radius: 8px;
|
|
69
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
70
|
+
}
|
|
71
|
+
</style>
|
|
72
|
+
</head>
|
|
73
|
+
|
|
74
|
+
<body>
|
|
75
|
+
<div class="header">
|
|
76
|
+
<div class="container">
|
|
77
|
+
<h1><%= link_to "Web Rake", root_path %></h1>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<div class="container">
|
|
82
|
+
<% if flash[:alert] %>
|
|
83
|
+
<div class="alert"><%= flash[:alert] %></div>
|
|
84
|
+
<% end %>
|
|
85
|
+
|
|
86
|
+
<% if flash[:notice] %>
|
|
87
|
+
<div class="notice"><%= flash[:notice] %></div>
|
|
88
|
+
<% end %>
|
|
89
|
+
|
|
90
|
+
<div class="content">
|
|
91
|
+
<%= yield %>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
</body>
|
|
95
|
+
</html>
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
<style>
|
|
2
|
+
.execution-header {
|
|
3
|
+
margin-bottom: 30px;
|
|
4
|
+
padding-bottom: 20px;
|
|
5
|
+
border-bottom: 2px solid #dee2e6;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.task-title {
|
|
9
|
+
font-size: 1.8rem;
|
|
10
|
+
color: #2c3e50;
|
|
11
|
+
margin-bottom: 10px;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.execution-summary {
|
|
15
|
+
display: flex;
|
|
16
|
+
gap: 20px;
|
|
17
|
+
margin-top: 15px;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.summary-item {
|
|
21
|
+
display: flex;
|
|
22
|
+
align-items: center;
|
|
23
|
+
gap: 5px;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.status-badge {
|
|
27
|
+
padding: 4px 12px;
|
|
28
|
+
border-radius: 4px;
|
|
29
|
+
font-weight: 600;
|
|
30
|
+
font-size: 0.9rem;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.status-success {
|
|
34
|
+
background-color: #d4edda;
|
|
35
|
+
color: #155724;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.status-error {
|
|
39
|
+
background-color: #f8d7da;
|
|
40
|
+
color: #721c24;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.duration-badge {
|
|
44
|
+
background-color: #d1ecf1;
|
|
45
|
+
color: #0c5460;
|
|
46
|
+
padding: 4px 12px;
|
|
47
|
+
border-radius: 4px;
|
|
48
|
+
font-size: 0.9rem;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.output-section {
|
|
52
|
+
margin-bottom: 30px;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.output-header {
|
|
56
|
+
font-size: 1.2rem;
|
|
57
|
+
color: #495057;
|
|
58
|
+
margin-bottom: 15px;
|
|
59
|
+
display: flex;
|
|
60
|
+
align-items: center;
|
|
61
|
+
gap: 10px;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.output-count {
|
|
65
|
+
background-color: #6c757d;
|
|
66
|
+
color: white;
|
|
67
|
+
padding: 2px 8px;
|
|
68
|
+
border-radius: 12px;
|
|
69
|
+
font-size: 0.75rem;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.output-box {
|
|
73
|
+
background-color: #f8f9fa;
|
|
74
|
+
border: 1px solid #dee2e6;
|
|
75
|
+
border-radius: 4px;
|
|
76
|
+
padding: 15px;
|
|
77
|
+
font-family: 'Courier New', monospace;
|
|
78
|
+
font-size: 0.9rem;
|
|
79
|
+
line-height: 1.5;
|
|
80
|
+
max-height: 400px;
|
|
81
|
+
overflow-y: auto;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.output-line {
|
|
85
|
+
margin-bottom: 4px;
|
|
86
|
+
white-space: pre-wrap;
|
|
87
|
+
word-break: break-all;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.error-output {
|
|
91
|
+
background-color: #fff5f5;
|
|
92
|
+
border-color: #feb2b2;
|
|
93
|
+
color: #742a2a;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.empty-output {
|
|
97
|
+
color: #6c757d;
|
|
98
|
+
font-style: italic;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.actions {
|
|
102
|
+
margin-top: 30px;
|
|
103
|
+
padding-top: 20px;
|
|
104
|
+
border-top: 2px solid #dee2e6;
|
|
105
|
+
display: flex;
|
|
106
|
+
gap: 10px;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.btn {
|
|
110
|
+
padding: 10px 20px;
|
|
111
|
+
border-radius: 4px;
|
|
112
|
+
text-decoration: none;
|
|
113
|
+
font-size: 1rem;
|
|
114
|
+
border: none;
|
|
115
|
+
cursor: pointer;
|
|
116
|
+
transition: all 0.3s ease;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.btn-primary {
|
|
120
|
+
background-color: #007bff;
|
|
121
|
+
color: white;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.btn-primary:hover {
|
|
125
|
+
background-color: #0056b3;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.btn-secondary {
|
|
129
|
+
background-color: #6c757d;
|
|
130
|
+
color: white;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.btn-secondary:hover {
|
|
134
|
+
background-color: #545b62;
|
|
135
|
+
}
|
|
136
|
+
</style>
|
|
137
|
+
|
|
138
|
+
<div class="execution-header">
|
|
139
|
+
<h2 class="task-title">Task: <%= task.name %></h2>
|
|
140
|
+
|
|
141
|
+
<div class="execution-summary">
|
|
142
|
+
<div class="summary-item">
|
|
143
|
+
<span>Status:</span>
|
|
144
|
+
<span class="status-badge <%= success ? 'status-success' : 'status-error' %>">
|
|
145
|
+
<%= success ? '✓ Success' : '✗ Failed' %>
|
|
146
|
+
</span>
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
<div class="summary-item">
|
|
150
|
+
<span>Duration:</span>
|
|
151
|
+
<span class="duration-badge"><%= duration %> seconds</span>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
<% if output.any? %>
|
|
157
|
+
<div class="output-section">
|
|
158
|
+
<h3 class="output-header">
|
|
159
|
+
Output
|
|
160
|
+
<span class="output-count"><%= output.count %> lines</span>
|
|
161
|
+
</h3>
|
|
162
|
+
<div class="output-box">
|
|
163
|
+
<% output.each do |line| %>
|
|
164
|
+
<div class="output-line"><%= line %></div>
|
|
165
|
+
<% end %>
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
<% else %>
|
|
169
|
+
<div class="output-section">
|
|
170
|
+
<h3 class="output-header">Output</h3>
|
|
171
|
+
<div class="output-box">
|
|
172
|
+
<div class="empty-output">No output generated</div>
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
<% end %>
|
|
176
|
+
|
|
177
|
+
<% if errors.any? %>
|
|
178
|
+
<div class="output-section">
|
|
179
|
+
<h3 class="output-header">
|
|
180
|
+
Errors
|
|
181
|
+
<span class="output-count"><%= errors.count %> lines</span>
|
|
182
|
+
</h3>
|
|
183
|
+
<div class="output-box error-output">
|
|
184
|
+
<% errors.each do |line| %>
|
|
185
|
+
<div class="output-line"><%= line %></div>
|
|
186
|
+
<% end %>
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
189
|
+
<% end %>
|
|
190
|
+
|
|
191
|
+
<div class="actions">
|
|
192
|
+
<%= link_to "Back to Tasks", root_path, class: "btn btn-primary" %>
|
|
193
|
+
</div>
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
<style>
|
|
2
|
+
.tasks-list {
|
|
3
|
+
list-style: none;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
.task-item {
|
|
7
|
+
padding: 15px;
|
|
8
|
+
margin-bottom: 10px;
|
|
9
|
+
background-color: #f8f9fa;
|
|
10
|
+
border-radius: 4px;
|
|
11
|
+
border: 1px solid #dee2e6;
|
|
12
|
+
transition: all 0.3s ease;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.task-item:hover {
|
|
16
|
+
background-color: #e9ecef;
|
|
17
|
+
transform: translateX(5px);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.task-header {
|
|
21
|
+
display: flex;
|
|
22
|
+
justify-content: space-between;
|
|
23
|
+
align-items: center;
|
|
24
|
+
margin-bottom: 8px;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.task-name {
|
|
28
|
+
font-weight: 600;
|
|
29
|
+
color: #2c3e50;
|
|
30
|
+
font-size: 1.1rem;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.task-description {
|
|
34
|
+
color: #6c757d;
|
|
35
|
+
margin-bottom: 10px;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.run-button {
|
|
39
|
+
background-color: #28a745;
|
|
40
|
+
color: white;
|
|
41
|
+
border: none;
|
|
42
|
+
padding: 8px 16px;
|
|
43
|
+
border-radius: 4px;
|
|
44
|
+
cursor: pointer;
|
|
45
|
+
font-size: 14px;
|
|
46
|
+
transition: background-color 0.3s ease;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.run-button:hover {
|
|
50
|
+
background-color: #218838;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.page-header {
|
|
54
|
+
margin-bottom: 30px;
|
|
55
|
+
padding-bottom: 20px;
|
|
56
|
+
border-bottom: 2px solid #dee2e6;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.page-title {
|
|
60
|
+
font-size: 2rem;
|
|
61
|
+
color: #2c3e50;
|
|
62
|
+
margin-bottom: 10px;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.page-subtitle {
|
|
66
|
+
color: #6c757d;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.empty-state {
|
|
70
|
+
text-align: center;
|
|
71
|
+
padding: 60px 20px;
|
|
72
|
+
color: #6c757d;
|
|
73
|
+
}
|
|
74
|
+
</style>
|
|
75
|
+
|
|
76
|
+
<div class="page-header">
|
|
77
|
+
<h2 class="page-title">
|
|
78
|
+
Available Rake Tasks
|
|
79
|
+
</h2>
|
|
80
|
+
<p class="page-subtitle">Click any task to run it. Showing db:seed and custom tasks from lib/tasks/</p>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<% if @tasks.any? %>
|
|
84
|
+
<ul class="tasks-list">
|
|
85
|
+
<% @tasks.each do |task| %>
|
|
86
|
+
<li class="task-item">
|
|
87
|
+
<div class="task-header">
|
|
88
|
+
<span class="task-name"><%= task.name %></span>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<% if task.comment.present? %>
|
|
92
|
+
<div class="task-description"><%= task.comment %></div>
|
|
93
|
+
<% end %>
|
|
94
|
+
|
|
95
|
+
<%= form_with url: execute_task_path(id: task.name.gsub(':', '__')),
|
|
96
|
+
method: :post,
|
|
97
|
+
local: true do |form| %>
|
|
98
|
+
<%= form.submit "Run Task", class: "run-button" %>
|
|
99
|
+
<% end %>
|
|
100
|
+
</li>
|
|
101
|
+
<% end %>
|
|
102
|
+
</ul>
|
|
103
|
+
<% else %>
|
|
104
|
+
<div class="empty-state">
|
|
105
|
+
<h3>No Custom Rake Tasks Found</h3>
|
|
106
|
+
<p>No rake tasks were found in your lib/tasks/ directory.</p>
|
|
107
|
+
<p>Create a .rake file in lib/tasks/ with tasks that depend on :environment.</p>
|
|
108
|
+
</div>
|
|
109
|
+
<% end %>
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module WebRake
|
|
2
|
+
class Engine < ::Rails::Engine
|
|
3
|
+
isolate_namespace WebRake
|
|
4
|
+
|
|
5
|
+
initializer 'web_rake.mount_routes' do |app|
|
|
6
|
+
app.routes.append do
|
|
7
|
+
mount WebRake::Engine, at: '/rails/tasks'
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
initializer 'web_rake.configure_credentials' do |app|
|
|
12
|
+
WebRake.username = ENV.fetch('WEB_RAKE_USERNAME', 'admin')
|
|
13
|
+
WebRake.password = ENV.fetch('WEB_RAKE_PASSWORD', 'password')
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
data/lib/web_rake.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: web_rake
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Ben Purinton
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 2025-09-15 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rails
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: 7.0.0
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: 7.0.0
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: rspec-rails
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: sqlite3
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0'
|
|
54
|
+
description: A Rails engine that provides a web interface for discovering and running
|
|
55
|
+
Rake tasks with HTTP basic authentication
|
|
56
|
+
email:
|
|
57
|
+
- ben@firstdraft.com
|
|
58
|
+
executables: []
|
|
59
|
+
extensions: []
|
|
60
|
+
extra_rdoc_files: []
|
|
61
|
+
files:
|
|
62
|
+
- MIT-LICENSE
|
|
63
|
+
- README.md
|
|
64
|
+
- Rakefile
|
|
65
|
+
- app/controllers/web_rake/application_controller.rb
|
|
66
|
+
- app/controllers/web_rake/tasks_controller.rb
|
|
67
|
+
- app/views/layouts/web_rake/application.html.erb
|
|
68
|
+
- app/views/web_rake/tasks/execute.html.erb
|
|
69
|
+
- app/views/web_rake/tasks/index.html.erb
|
|
70
|
+
- config/routes.rb
|
|
71
|
+
- lib/web_rake.rb
|
|
72
|
+
- lib/web_rake/engine.rb
|
|
73
|
+
- lib/web_rake/version.rb
|
|
74
|
+
homepage: https://github.com/firstdraft/web_rake
|
|
75
|
+
licenses:
|
|
76
|
+
- MIT
|
|
77
|
+
metadata:
|
|
78
|
+
homepage_uri: https://github.com/firstdraft/web_rake
|
|
79
|
+
source_code_uri: https://github.com/firstdraft/web_rake
|
|
80
|
+
changelog_uri: https://github.com/firstdraft/web_rake/blob/main/CHANGELOG.md
|
|
81
|
+
rdoc_options: []
|
|
82
|
+
require_paths:
|
|
83
|
+
- lib
|
|
84
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - ">="
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: 2.7.0
|
|
89
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
90
|
+
requirements:
|
|
91
|
+
- - ">="
|
|
92
|
+
- !ruby/object:Gem::Version
|
|
93
|
+
version: '0'
|
|
94
|
+
requirements: []
|
|
95
|
+
rubygems_version: 3.6.5
|
|
96
|
+
specification_version: 4
|
|
97
|
+
summary: Web interface for running Rake tasks
|
|
98
|
+
test_files: []
|