ruby_workspace_manager 0.3.0 → 0.4.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/README.md +4 -1
- data/completions/rwm.bash +169 -0
- data/completions/rwm.zsh +125 -0
- data/lib/rwm/affected_detector.rb +8 -3
- data/lib/rwm/cli.rb +32 -21
- data/lib/rwm/commands/affected.rb +5 -1
- data/lib/rwm/commands/cache.rb +45 -0
- data/lib/rwm/commands/graph.rb +2 -2
- data/lib/rwm/commands/init.rb +2 -1
- data/lib/rwm/commands/new.rb +8 -5
- data/lib/rwm/commands/run.rb +40 -15
- data/lib/rwm/dependency_graph.rb +7 -21
- data/lib/rwm/package.rb +0 -18
- data/lib/rwm/task_cache.rb +41 -10
- data/lib/rwm/task_runner.rb +38 -2
- data/lib/rwm/version.rb +1 -1
- data/lib/rwm/workspace.rb +0 -8
- data/lib/rwm.rb +14 -0
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 12c7c3a3f727bda561d67cc8ec9892663a61678cd50ca37e0cd236f90b7cd9c7
|
|
4
|
+
data.tar.gz: e72a42cee147558fb709c4fff8216ac3ce029d8284e4fc0f85f165c084bf7542
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1b947c3056778412444e3a0b135902bbd2e00241d8ba284fc4f8434865de9ca232aa0b23d526e9a1da3e5a71f661d2df51b2c45c1f5bff9910051c50809535e8
|
|
7
|
+
data.tar.gz: fe2b3e0ebc4267ce1c7a66d9d0e3eced4d673fe1bd489da85bd19c3b32a7c60ef4096890612eafb0787fedd69c4a095e380271b1760515ab5e158a95d12d2aa9
|
data/README.md
CHANGED
|
@@ -18,12 +18,15 @@ RWM manages Ruby monorepos with multiple apps and libraries. It builds a depende
|
|
|
18
18
|
| `rwm new <app\|lib> <name>` | Scaffold a new package. |
|
|
19
19
|
| `rwm graph` | Rebuild the dependency graph. `--dot` / `--mermaid` for visualization. |
|
|
20
20
|
| `rwm run <task> [pkg]` | Run a Rake task across packages. Packages without the task are skipped. |
|
|
21
|
-
| `rwm
|
|
21
|
+
| `rwm <task> [pkg]` | Any unknown command is a task shortcut: `rwm test` = `rwm run test`. |
|
|
22
22
|
| `rwm run <task> --affected` | Run only on packages affected by current changes. |
|
|
23
23
|
| `rwm check` | Validate conventions. |
|
|
24
24
|
| `rwm list` | List all packages. |
|
|
25
25
|
| `rwm info <name>` | Show package details. |
|
|
26
26
|
| `rwm affected` | Show affected packages. |
|
|
27
|
+
| `rwm cache clean [pkg]` | Clear cached task results. |
|
|
28
|
+
|
|
29
|
+
Shell completions for Bash and Zsh are included — see [GUIDE.md](GUIDE.md) for setup instructions.
|
|
27
30
|
|
|
28
31
|
See [GUIDE.md](GUIDE.md) for full usage documentation — dependencies, caching, affected detection, git hooks, design decisions, and more.
|
|
29
32
|
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# Bash completion for rwm (Ruby Workspace Manager)
|
|
2
|
+
# Source this file in your .bashrc or .bash_profile:
|
|
3
|
+
# source "$(gem contents ruby_workspace_manager | grep rwm.bash)"
|
|
4
|
+
|
|
5
|
+
_rwm_find_workspace_root() {
|
|
6
|
+
local dir="$PWD"
|
|
7
|
+
while [[ "$dir" != "/" ]]; do
|
|
8
|
+
if [[ -d "$dir/.git" ]]; then
|
|
9
|
+
echo "$dir"
|
|
10
|
+
return
|
|
11
|
+
fi
|
|
12
|
+
dir="$(dirname "$dir")"
|
|
13
|
+
done
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
_rwm_packages() {
|
|
17
|
+
local root
|
|
18
|
+
root="$(_rwm_find_workspace_root)"
|
|
19
|
+
[[ -z "$root" ]] && return
|
|
20
|
+
|
|
21
|
+
local dir
|
|
22
|
+
for dir in "$root"/libs/*/ "$root"/apps/*/; do
|
|
23
|
+
[[ -d "$dir" && -f "$dir/Gemfile" ]] && echo "${dir%/}" | xargs basename
|
|
24
|
+
done
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
_rwm() {
|
|
28
|
+
local cur prev words cword
|
|
29
|
+
_init_completion || return
|
|
30
|
+
|
|
31
|
+
local commands="init bootstrap new info graph check list run affected cache help version"
|
|
32
|
+
local global_flags="--verbose --help --version"
|
|
33
|
+
|
|
34
|
+
# Find the command (first non-flag argument after rwm)
|
|
35
|
+
local cmd="" cmd_index=0
|
|
36
|
+
local i
|
|
37
|
+
for ((i = 1; i < cword; i++)); do
|
|
38
|
+
case "${words[i]}" in
|
|
39
|
+
--verbose) ;;
|
|
40
|
+
-*) ;;
|
|
41
|
+
*)
|
|
42
|
+
cmd="${words[i]}"
|
|
43
|
+
cmd_index=$i
|
|
44
|
+
break
|
|
45
|
+
;;
|
|
46
|
+
esac
|
|
47
|
+
done
|
|
48
|
+
|
|
49
|
+
# No command yet — complete commands and global flags
|
|
50
|
+
if [[ -z "$cmd" ]]; then
|
|
51
|
+
COMPREPLY=($(compgen -W "$commands $global_flags" -- "$cur"))
|
|
52
|
+
return
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
# Per-command completions
|
|
56
|
+
case "$cmd" in
|
|
57
|
+
init)
|
|
58
|
+
COMPREPLY=($(compgen -W "--vscode" -- "$cur"))
|
|
59
|
+
;;
|
|
60
|
+
bootstrap | check | list | help | version)
|
|
61
|
+
;;
|
|
62
|
+
new)
|
|
63
|
+
local pos=$((cword - cmd_index))
|
|
64
|
+
if [[ $pos -eq 1 ]]; then
|
|
65
|
+
COMPREPLY=($(compgen -W "app lib" -- "$cur"))
|
|
66
|
+
fi
|
|
67
|
+
;;
|
|
68
|
+
info)
|
|
69
|
+
local pos=$((cword - cmd_index))
|
|
70
|
+
if [[ $pos -eq 1 ]]; then
|
|
71
|
+
COMPREPLY=($(compgen -W "$(_rwm_packages)" -- "$cur"))
|
|
72
|
+
fi
|
|
73
|
+
;;
|
|
74
|
+
graph)
|
|
75
|
+
COMPREPLY=($(compgen -W "--dot --mermaid" -- "$cur"))
|
|
76
|
+
;;
|
|
77
|
+
affected)
|
|
78
|
+
case "$prev" in
|
|
79
|
+
--base) ;;
|
|
80
|
+
*)
|
|
81
|
+
COMPREPLY=($(compgen -W "--base --committed" -- "$cur"))
|
|
82
|
+
;;
|
|
83
|
+
esac
|
|
84
|
+
;;
|
|
85
|
+
cache)
|
|
86
|
+
local pos=$((cword - cmd_index))
|
|
87
|
+
if [[ $pos -eq 1 ]]; then
|
|
88
|
+
COMPREPLY=($(compgen -W "clean" -- "$cur"))
|
|
89
|
+
elif [[ $pos -eq 2 ]]; then
|
|
90
|
+
COMPREPLY=($(compgen -W "$(_rwm_packages)" -- "$cur"))
|
|
91
|
+
fi
|
|
92
|
+
;;
|
|
93
|
+
run)
|
|
94
|
+
local run_flags="--affected --committed --base --dry-run --no-cache --buffered --concurrency"
|
|
95
|
+
case "$prev" in
|
|
96
|
+
--base | --concurrency)
|
|
97
|
+
# These flags expect a value; don't complete
|
|
98
|
+
;;
|
|
99
|
+
*)
|
|
100
|
+
# Count positional args (skip flags and flag values)
|
|
101
|
+
local positionals=0
|
|
102
|
+
local skip_next=false
|
|
103
|
+
for ((i = cmd_index + 1; i < cword; i++)); do
|
|
104
|
+
if $skip_next; then
|
|
105
|
+
skip_next=false
|
|
106
|
+
continue
|
|
107
|
+
fi
|
|
108
|
+
case "${words[i]}" in
|
|
109
|
+
--base | --concurrency)
|
|
110
|
+
skip_next=true
|
|
111
|
+
;;
|
|
112
|
+
-*)
|
|
113
|
+
;;
|
|
114
|
+
*)
|
|
115
|
+
((positionals++))
|
|
116
|
+
;;
|
|
117
|
+
esac
|
|
118
|
+
done
|
|
119
|
+
|
|
120
|
+
if [[ "$cur" == -* ]]; then
|
|
121
|
+
COMPREPLY=($(compgen -W "$run_flags" -- "$cur"))
|
|
122
|
+
elif [[ $positionals -eq 0 ]]; then
|
|
123
|
+
# First positional: task name (no completion — user-defined)
|
|
124
|
+
:
|
|
125
|
+
elif [[ $positionals -eq 1 ]]; then
|
|
126
|
+
# Second positional: package name
|
|
127
|
+
COMPREPLY=($(compgen -W "$(_rwm_packages)" -- "$cur"))
|
|
128
|
+
fi
|
|
129
|
+
;;
|
|
130
|
+
esac
|
|
131
|
+
;;
|
|
132
|
+
*)
|
|
133
|
+
# Any unrecognized command is a task shortcut — offer run flags and packages
|
|
134
|
+
local run_flags="--affected --committed --base --dry-run --no-cache --buffered --concurrency"
|
|
135
|
+
case "$prev" in
|
|
136
|
+
--base | --concurrency)
|
|
137
|
+
;;
|
|
138
|
+
*)
|
|
139
|
+
local positionals=0
|
|
140
|
+
local skip_next=false
|
|
141
|
+
for ((i = cmd_index + 1; i < cword; i++)); do
|
|
142
|
+
if $skip_next; then
|
|
143
|
+
skip_next=false
|
|
144
|
+
continue
|
|
145
|
+
fi
|
|
146
|
+
case "${words[i]}" in
|
|
147
|
+
--base | --concurrency)
|
|
148
|
+
skip_next=true
|
|
149
|
+
;;
|
|
150
|
+
-*)
|
|
151
|
+
;;
|
|
152
|
+
*)
|
|
153
|
+
((positionals++))
|
|
154
|
+
;;
|
|
155
|
+
esac
|
|
156
|
+
done
|
|
157
|
+
|
|
158
|
+
if [[ "$cur" == -* ]]; then
|
|
159
|
+
COMPREPLY=($(compgen -W "$run_flags" -- "$cur"))
|
|
160
|
+
elif [[ $positionals -eq 0 ]]; then
|
|
161
|
+
COMPREPLY=($(compgen -W "$(_rwm_packages)" -- "$cur"))
|
|
162
|
+
fi
|
|
163
|
+
;;
|
|
164
|
+
esac
|
|
165
|
+
;;
|
|
166
|
+
esac
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
complete -F _rwm rwm
|
data/completions/rwm.zsh
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
#compdef rwm
|
|
2
|
+
# Zsh completion for rwm (Ruby Workspace Manager)
|
|
3
|
+
# Add the completions directory to your fpath in .zshrc:
|
|
4
|
+
# fpath=($(gem contents ruby_workspace_manager | grep completions/rwm.zsh | xargs dirname) $fpath)
|
|
5
|
+
# autoload -Uz compinit && compinit
|
|
6
|
+
|
|
7
|
+
_rwm_find_workspace_root() {
|
|
8
|
+
local dir="${PWD}"
|
|
9
|
+
while [[ "$dir" != "/" ]]; do
|
|
10
|
+
if [[ -d "$dir/.git" ]]; then
|
|
11
|
+
echo "$dir"
|
|
12
|
+
return
|
|
13
|
+
fi
|
|
14
|
+
dir="${dir:h}"
|
|
15
|
+
done
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
_rwm_complete_packages() {
|
|
19
|
+
local root
|
|
20
|
+
root="$(_rwm_find_workspace_root)"
|
|
21
|
+
[[ -z "$root" ]] && return
|
|
22
|
+
|
|
23
|
+
local -a packages
|
|
24
|
+
local dir
|
|
25
|
+
for dir in "$root"/libs/*(N/) "$root"/apps/*(N/); do
|
|
26
|
+
[[ -f "$dir/Gemfile" ]] && packages+=("${dir:t}")
|
|
27
|
+
done
|
|
28
|
+
_describe 'package' packages
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
_rwm() {
|
|
32
|
+
local -a commands=(
|
|
33
|
+
'init:Initialize a new rwm workspace'
|
|
34
|
+
'bootstrap:Install deps and run bootstrap tasks in all packages'
|
|
35
|
+
'new:Scaffold a new app or lib'
|
|
36
|
+
'info:Show details about a package'
|
|
37
|
+
'graph:Build and save the dependency graph'
|
|
38
|
+
'check:Validate dependency graph and conventions'
|
|
39
|
+
'list:List all packages in the workspace'
|
|
40
|
+
'run:Run a rake task across all or one package'
|
|
41
|
+
'affected:Show packages affected by current changes'
|
|
42
|
+
'cache:Manage task cache'
|
|
43
|
+
'help:Show help'
|
|
44
|
+
'version:Show version'
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
local -a global_flags=(
|
|
48
|
+
'--verbose[Enable debug logging]'
|
|
49
|
+
'--help[Show help]'
|
|
50
|
+
'--version[Show version]'
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
local -a run_flags=(
|
|
54
|
+
'--affected[Only run on affected packages]'
|
|
55
|
+
'--committed[Only consider committed changes]'
|
|
56
|
+
'--base[Compare against REF instead of auto-detected base]:ref'
|
|
57
|
+
'--dry-run[Show what would run without executing]'
|
|
58
|
+
'--no-cache[Bypass task-level caching]'
|
|
59
|
+
'--buffered[Buffer output per-package and print on completion]'
|
|
60
|
+
'--concurrency[Max parallel workers]:number'
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# If we haven't completed the first argument yet, offer commands
|
|
64
|
+
if (( CURRENT == 2 )); then
|
|
65
|
+
_describe 'command' commands
|
|
66
|
+
return
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
local cmd="${words[2]}"
|
|
70
|
+
|
|
71
|
+
case "$cmd" in
|
|
72
|
+
init)
|
|
73
|
+
_arguments -s \
|
|
74
|
+
'--vscode[Generate VSCode .code-workspace file]' \
|
|
75
|
+
$global_flags
|
|
76
|
+
;;
|
|
77
|
+
bootstrap | check | list | help | version)
|
|
78
|
+
;;
|
|
79
|
+
new)
|
|
80
|
+
local -a types=('app:Application package' 'lib:Library package')
|
|
81
|
+
case $CURRENT in
|
|
82
|
+
3) _describe 'type' types ;;
|
|
83
|
+
4) _message 'package name' ;;
|
|
84
|
+
esac
|
|
85
|
+
;;
|
|
86
|
+
info)
|
|
87
|
+
if (( CURRENT == 3 )); then
|
|
88
|
+
_rwm_complete_packages
|
|
89
|
+
fi
|
|
90
|
+
;;
|
|
91
|
+
graph)
|
|
92
|
+
_arguments -s \
|
|
93
|
+
'--dot[Output in Graphviz DOT format]' \
|
|
94
|
+
'--mermaid[Output in Mermaid format]' \
|
|
95
|
+
$global_flags
|
|
96
|
+
;;
|
|
97
|
+
affected)
|
|
98
|
+
_arguments -s \
|
|
99
|
+
'--base[Compare against REF instead of auto-detected base]:ref' \
|
|
100
|
+
'--committed[Only consider committed changes]' \
|
|
101
|
+
$global_flags
|
|
102
|
+
;;
|
|
103
|
+
cache)
|
|
104
|
+
case $CURRENT in
|
|
105
|
+
3) local -a subcmds=('clean:Clear cached task results')
|
|
106
|
+
_describe 'subcommand' subcmds ;;
|
|
107
|
+
4) _rwm_complete_packages ;;
|
|
108
|
+
esac
|
|
109
|
+
;;
|
|
110
|
+
run)
|
|
111
|
+
_arguments -s \
|
|
112
|
+
$run_flags \
|
|
113
|
+
'1:task' \
|
|
114
|
+
'2:package:_rwm_complete_packages'
|
|
115
|
+
;;
|
|
116
|
+
*)
|
|
117
|
+
# Any unrecognized command is a task shortcut — offer run flags and packages
|
|
118
|
+
_arguments -s \
|
|
119
|
+
$run_flags \
|
|
120
|
+
'1:package:_rwm_complete_packages'
|
|
121
|
+
;;
|
|
122
|
+
esac
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
_rwm "$@"
|
|
@@ -6,11 +6,11 @@ module Rwm
|
|
|
6
6
|
class AffectedDetector
|
|
7
7
|
attr_reader :workspace, :graph, :base_branch
|
|
8
8
|
|
|
9
|
-
def initialize(workspace, graph, committed_only: false)
|
|
9
|
+
def initialize(workspace, graph, committed_only: false, base_branch: nil)
|
|
10
10
|
@workspace = workspace
|
|
11
11
|
@graph = graph
|
|
12
12
|
@committed_only = committed_only
|
|
13
|
-
@base_branch = detect_base_branch
|
|
13
|
+
@base_branch = base_branch || detect_base_branch
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
# Returns packages directly changed + their transitive dependents
|
|
@@ -68,20 +68,25 @@ module Rwm
|
|
|
68
68
|
files = Set.new
|
|
69
69
|
|
|
70
70
|
# 1. Committed changes: base branch vs HEAD
|
|
71
|
+
Rwm.debug("affected: git diff --name-only #{base_branch}...HEAD")
|
|
71
72
|
committed, _, status = Open3.capture3("git", "-C", workspace.root, "diff", "--name-only", "#{base_branch}...HEAD")
|
|
72
73
|
committed.lines.each { |l| files << l.chomp } if status.success?
|
|
73
74
|
|
|
74
75
|
unless @committed_only
|
|
75
76
|
# 2. Staged changes (not yet committed)
|
|
77
|
+
Rwm.debug("affected: git diff --name-only --cached")
|
|
76
78
|
staged, _, status = Open3.capture3("git", "-C", workspace.root, "diff", "--name-only", "--cached")
|
|
77
79
|
staged.lines.each { |l| files << l.chomp } if status.success?
|
|
78
80
|
|
|
79
81
|
# 3. Unstaged working directory changes
|
|
82
|
+
Rwm.debug("affected: git diff --name-only")
|
|
80
83
|
unstaged, _, status = Open3.capture3("git", "-C", workspace.root, "diff", "--name-only")
|
|
81
84
|
unstaged.lines.each { |l| files << l.chomp } if status.success?
|
|
82
85
|
end
|
|
83
86
|
|
|
84
|
-
files.reject(&:empty?).to_a
|
|
87
|
+
result = files.reject(&:empty?).to_a
|
|
88
|
+
Rwm.debug("affected: #{result.size} changed file(s) detected")
|
|
89
|
+
result
|
|
85
90
|
end
|
|
86
91
|
|
|
87
92
|
def map_files_to_packages(files)
|
data/lib/rwm/cli.rb
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "optparse"
|
|
4
|
-
|
|
5
3
|
module Rwm
|
|
6
4
|
class CLI
|
|
7
5
|
COMMANDS = {
|
|
@@ -13,12 +11,10 @@ module Rwm
|
|
|
13
11
|
"check" => "Commands::Check",
|
|
14
12
|
"list" => "Commands::List",
|
|
15
13
|
"run" => "Commands::Run",
|
|
16
|
-
"affected" => "Commands::Affected"
|
|
14
|
+
"affected" => "Commands::Affected",
|
|
15
|
+
"cache" => "Commands::Cache"
|
|
17
16
|
}.freeze
|
|
18
17
|
|
|
19
|
-
# Shortcuts that expand to `run <task>`
|
|
20
|
-
TASK_SHORTCUTS = %w[test spec build].freeze
|
|
21
|
-
|
|
22
18
|
def self.run(argv)
|
|
23
19
|
new(argv).run
|
|
24
20
|
end
|
|
@@ -28,6 +24,8 @@ module Rwm
|
|
|
28
24
|
end
|
|
29
25
|
|
|
30
26
|
def run
|
|
27
|
+
parse_global_flags
|
|
28
|
+
|
|
31
29
|
command_name = @argv.shift
|
|
32
30
|
|
|
33
31
|
if command_name.nil? || %w[-h --help help].include?(command_name)
|
|
@@ -42,21 +40,15 @@ module Rwm
|
|
|
42
40
|
|
|
43
41
|
check_required_tools
|
|
44
42
|
|
|
45
|
-
#
|
|
46
|
-
|
|
43
|
+
# Unknown commands are treated as task names: `rwm test` → `rwm run test`
|
|
44
|
+
unless COMMANDS.key?(command_name)
|
|
47
45
|
@argv.unshift(command_name)
|
|
48
46
|
command_name = "run"
|
|
49
47
|
end
|
|
50
48
|
|
|
51
|
-
const_name = COMMANDS[command_name]
|
|
52
|
-
unless const_name
|
|
53
|
-
$stderr.puts "Unknown command: #{command_name}"
|
|
54
|
-
$stderr.puts "Run `rwm help` for available commands."
|
|
55
|
-
return 1
|
|
56
|
-
end
|
|
57
|
-
|
|
58
49
|
# Autoload the command
|
|
59
50
|
require "rwm/commands/#{command_name}"
|
|
51
|
+
const_name = COMMANDS[command_name]
|
|
60
52
|
command_class = const_name.split("::").reduce(Rwm) { |mod, name| mod.const_get(name) }
|
|
61
53
|
command_class.new(@argv).run
|
|
62
54
|
rescue Rwm::Error => e
|
|
@@ -66,11 +58,17 @@ module Rwm
|
|
|
66
58
|
|
|
67
59
|
private
|
|
68
60
|
|
|
61
|
+
def parse_global_flags
|
|
62
|
+
Rwm.verbose = true if ENV["RWM_DEBUG"] == "1"
|
|
63
|
+
if @argv.delete("--verbose")
|
|
64
|
+
Rwm.verbose = true
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
69
68
|
def check_required_tools
|
|
70
69
|
%w[git bundle].each do |tool|
|
|
71
70
|
unless system("which", tool, out: File::NULL, err: File::NULL)
|
|
72
|
-
|
|
73
|
-
exit 1
|
|
71
|
+
raise Rwm::Error, "#{tool} is not installed or not in PATH."
|
|
74
72
|
end
|
|
75
73
|
end
|
|
76
74
|
end
|
|
@@ -82,7 +80,7 @@ module Rwm
|
|
|
82
80
|
Usage: rwm <command> [options]
|
|
83
81
|
|
|
84
82
|
Commands:
|
|
85
|
-
init
|
|
83
|
+
init [--vscode] Initialize a new rwm workspace
|
|
86
84
|
bootstrap Install deps and run bootstrap tasks in all packages
|
|
87
85
|
new <type> <name> Scaffold a new app or lib
|
|
88
86
|
info <name> Show details about a package
|
|
@@ -91,18 +89,31 @@ module Rwm
|
|
|
91
89
|
--mermaid Output in Mermaid format
|
|
92
90
|
check Validate dependency graph and conventions
|
|
93
91
|
run <task> [pkg] Run a rake task across all (or one) package(s)
|
|
94
|
-
test Shortcut for `rwm run test`
|
|
95
92
|
affected Show packages affected by current changes
|
|
93
|
+
--base REF Compare against REF instead of auto-detected base
|
|
94
|
+
--committed Only consider committed changes
|
|
96
95
|
list List all packages in the workspace
|
|
96
|
+
cache clean [pkg] Clear cached task results
|
|
97
97
|
help Show this help
|
|
98
|
+
version Show version
|
|
99
|
+
|
|
100
|
+
Any unrecognized command is treated as a task name:
|
|
101
|
+
rwm test → rwm run test
|
|
102
|
+
rwm lint → rwm run lint
|
|
98
103
|
|
|
99
|
-
Run options:
|
|
104
|
+
Run options (for `rwm run` and task shortcuts):
|
|
100
105
|
--affected Only run on affected packages
|
|
106
|
+
--committed Only consider committed changes (with --affected)
|
|
107
|
+
--base REF Compare against REF instead of auto-detected base
|
|
108
|
+
--dry-run Show what would run without executing
|
|
101
109
|
--no-cache Bypass task-level caching
|
|
110
|
+
--buffered Buffer output per-package
|
|
111
|
+
--concurrency N Max parallel workers (default: CPU count)
|
|
102
112
|
|
|
103
|
-
|
|
113
|
+
Global options:
|
|
104
114
|
-h, --help Show this help
|
|
105
115
|
-v, --version Show version
|
|
116
|
+
--verbose Enable debug logging (or set RWM_DEBUG=1)
|
|
106
117
|
HELP
|
|
107
118
|
end
|
|
108
119
|
end
|
|
@@ -8,13 +8,14 @@ module Rwm
|
|
|
8
8
|
def initialize(argv)
|
|
9
9
|
@argv = argv
|
|
10
10
|
@committed_only = false
|
|
11
|
+
@base_branch = nil
|
|
11
12
|
parse_options
|
|
12
13
|
end
|
|
13
14
|
|
|
14
15
|
def run
|
|
15
16
|
workspace = Workspace.find
|
|
16
17
|
graph = DependencyGraph.load(workspace)
|
|
17
|
-
detector = AffectedDetector.new(workspace, graph, committed_only: @committed_only)
|
|
18
|
+
detector = AffectedDetector.new(workspace, graph, committed_only: @committed_only, base_branch: @base_branch)
|
|
18
19
|
|
|
19
20
|
affected = detector.affected_packages
|
|
20
21
|
directly_changed = detector.directly_changed_packages
|
|
@@ -40,6 +41,9 @@ module Rwm
|
|
|
40
41
|
|
|
41
42
|
def parse_options
|
|
42
43
|
parser = OptionParser.new do |opts|
|
|
44
|
+
opts.on("--base REF", "Compare against REF instead of auto-detected base branch") do |ref|
|
|
45
|
+
@base_branch = ref
|
|
46
|
+
end
|
|
43
47
|
opts.on("--committed", "Only consider committed changes (ignore staged/unstaged)") do
|
|
44
48
|
@committed_only = true
|
|
45
49
|
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rwm
|
|
4
|
+
module Commands
|
|
5
|
+
class Cache
|
|
6
|
+
def initialize(argv)
|
|
7
|
+
@argv = argv
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def run
|
|
11
|
+
subcommand = @argv.shift
|
|
12
|
+
|
|
13
|
+
case subcommand
|
|
14
|
+
when "clean"
|
|
15
|
+
clean
|
|
16
|
+
when nil
|
|
17
|
+
$stderr.puts "Usage: rwm cache clean [<package>]"
|
|
18
|
+
1
|
|
19
|
+
else
|
|
20
|
+
$stderr.puts "Unknown cache subcommand: #{subcommand}"
|
|
21
|
+
$stderr.puts "Usage: rwm cache clean [<package>]"
|
|
22
|
+
1
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def clean
|
|
29
|
+
workspace = Workspace.find
|
|
30
|
+
package_name = @argv.shift
|
|
31
|
+
|
|
32
|
+
if package_name
|
|
33
|
+
workspace.find_package(package_name) # validates package exists
|
|
34
|
+
TaskCache.clean(workspace, package_name: package_name)
|
|
35
|
+
puts "Cleared cache for #{package_name}."
|
|
36
|
+
else
|
|
37
|
+
TaskCache.clean(workspace)
|
|
38
|
+
puts "Cleared all cached task results."
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
0
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
data/lib/rwm/commands/graph.rb
CHANGED
data/lib/rwm/commands/init.rb
CHANGED
|
@@ -44,9 +44,10 @@ module Rwm
|
|
|
44
44
|
puts "Workspace initialized. Running bootstrap..."
|
|
45
45
|
puts
|
|
46
46
|
|
|
47
|
-
# Call bootstrap as the last step
|
|
47
|
+
# Call bootstrap as the last step — return its exit code
|
|
48
48
|
require "rwm/commands/bootstrap"
|
|
49
49
|
Commands::Bootstrap.new([]).run
|
|
50
|
+
0
|
|
50
51
|
end
|
|
51
52
|
|
|
52
53
|
private
|
data/lib/rwm/commands/new.rb
CHANGED
|
@@ -53,13 +53,14 @@ module Rwm
|
|
|
53
53
|
end
|
|
54
54
|
|
|
55
55
|
def scaffold(pkg_path, name, type)
|
|
56
|
-
|
|
56
|
+
source_dir = type == "lib" ? "lib" : "app"
|
|
57
|
+
FileUtils.mkdir_p(File.join(pkg_path, source_dir, name))
|
|
57
58
|
FileUtils.mkdir_p(File.join(pkg_path, "spec"))
|
|
58
59
|
|
|
59
60
|
write_gemfile(pkg_path, name)
|
|
60
61
|
write_gemspec(pkg_path, name, type)
|
|
61
62
|
write_rakefile(pkg_path, name)
|
|
62
|
-
|
|
63
|
+
write_entry_file(pkg_path, name, type)
|
|
63
64
|
write_spec_helper(pkg_path)
|
|
64
65
|
end
|
|
65
66
|
|
|
@@ -94,8 +95,9 @@ module Rwm
|
|
|
94
95
|
'',
|
|
95
96
|
]
|
|
96
97
|
lines << ' spec.files = Dir.glob("lib/**/*")' if type == "lib"
|
|
98
|
+
source_dir = type == "lib" ? "lib" : "app"
|
|
97
99
|
lines.concat([
|
|
98
|
-
|
|
100
|
+
" spec.require_paths = [\"#{source_dir}\"]",
|
|
99
101
|
' spec.required_ruby_version = ">= 3.4.0"',
|
|
100
102
|
'end',
|
|
101
103
|
'',
|
|
@@ -121,8 +123,9 @@ module Rwm
|
|
|
121
123
|
RAKEFILE
|
|
122
124
|
end
|
|
123
125
|
|
|
124
|
-
def
|
|
125
|
-
|
|
126
|
+
def write_entry_file(pkg_path, name, type)
|
|
127
|
+
source_dir = type == "lib" ? "lib" : "app"
|
|
128
|
+
File.write(File.join(pkg_path, source_dir, "#{name}.rb"), <<~RUBY)
|
|
126
129
|
# frozen_string_literal: true
|
|
127
130
|
|
|
128
131
|
module #{camelize(name)}
|
data/lib/rwm/commands/run.rb
CHANGED
|
@@ -12,6 +12,8 @@ module Rwm
|
|
|
12
12
|
@no_cache = false
|
|
13
13
|
@buffered = false
|
|
14
14
|
@concurrency = nil
|
|
15
|
+
@dry_run = false
|
|
16
|
+
@base_branch = nil
|
|
15
17
|
parse_options
|
|
16
18
|
end
|
|
17
19
|
|
|
@@ -19,7 +21,7 @@ module Rwm
|
|
|
19
21
|
task = @argv.shift
|
|
20
22
|
|
|
21
23
|
unless task
|
|
22
|
-
$stderr.puts "Usage: rwm run <task> [<package>] [--affected] [--no-cache] [--buffered] [--concurrency N]"
|
|
24
|
+
$stderr.puts "Usage: rwm run <task> [<package>] [--affected] [--base REF] [--dry-run] [--no-cache] [--buffered] [--concurrency N]"
|
|
23
25
|
return 1
|
|
24
26
|
end
|
|
25
27
|
|
|
@@ -29,14 +31,9 @@ module Rwm
|
|
|
29
31
|
graph = DependencyGraph.load(workspace)
|
|
30
32
|
|
|
31
33
|
packages = if package_name
|
|
32
|
-
|
|
33
|
-
unless pkg
|
|
34
|
-
$stderr.puts "Unknown package: #{package_name}"
|
|
35
|
-
return 1
|
|
36
|
-
end
|
|
37
|
-
[pkg]
|
|
34
|
+
[workspace.find_package(package_name)]
|
|
38
35
|
elsif @affected_only
|
|
39
|
-
detector = AffectedDetector.new(workspace, graph, committed_only: @committed_only)
|
|
36
|
+
detector = AffectedDetector.new(workspace, graph, committed_only: @committed_only, base_branch: @base_branch)
|
|
40
37
|
affected = detector.affected_packages
|
|
41
38
|
if affected.empty?
|
|
42
39
|
puts "No affected packages. Nothing to run."
|
|
@@ -53,13 +50,15 @@ module Rwm
|
|
|
53
50
|
return 0
|
|
54
51
|
end
|
|
55
52
|
|
|
56
|
-
# Filter to packages
|
|
57
|
-
runnable = packages.select
|
|
53
|
+
# Filter to packages with a Rakefile (skip those without one entirely)
|
|
54
|
+
runnable = packages.select(&:has_rakefile?)
|
|
58
55
|
if runnable.empty?
|
|
59
|
-
puts "No packages with a
|
|
56
|
+
puts "No packages with a Rakefile found."
|
|
60
57
|
return 0
|
|
61
58
|
end
|
|
62
59
|
|
|
60
|
+
Rwm.debug("run: #{runnable.size} package(s) with Rakefiles")
|
|
61
|
+
|
|
63
62
|
# Auto-detect cacheable tasks unless --no-cache
|
|
64
63
|
cache = TaskCache.new(workspace, graph) unless @no_cache
|
|
65
64
|
if cache
|
|
@@ -74,6 +73,12 @@ module Rwm
|
|
|
74
73
|
return 0
|
|
75
74
|
end
|
|
76
75
|
|
|
76
|
+
if @dry_run
|
|
77
|
+
puts "Dry run: would run `rake #{task}` on #{runnable.size} package(s):"
|
|
78
|
+
runnable.each { |pkg| puts " #{pkg.name}" }
|
|
79
|
+
return 0
|
|
80
|
+
end
|
|
81
|
+
|
|
77
82
|
puts "Running `rake #{task}` across #{runnable.size} package(s)..."
|
|
78
83
|
puts
|
|
79
84
|
|
|
@@ -86,19 +91,33 @@ module Rwm
|
|
|
86
91
|
if cache
|
|
87
92
|
runner.results.each do |result|
|
|
88
93
|
next unless result.success
|
|
94
|
+
next if result.skipped
|
|
89
95
|
|
|
90
96
|
pkg = workspace.find_package(result.package_name)
|
|
91
97
|
cache.store(pkg, task) if cache.cacheable?(pkg, task)
|
|
92
98
|
end
|
|
93
99
|
end
|
|
94
100
|
|
|
101
|
+
passed, rest = runner.results.partition { |r| r.success && !r.skipped }
|
|
102
|
+
failed, skipped = rest.partition { |r| !r.success }
|
|
103
|
+
skipped_from_rest = runner.results.select(&:skipped)
|
|
104
|
+
|
|
105
|
+
total = runner.results.size
|
|
106
|
+
parts = []
|
|
107
|
+
parts << "#{passed.size} passed" unless passed.empty?
|
|
108
|
+
parts << "#{failed.size} failed" unless failed.empty?
|
|
109
|
+
parts << "#{skipped_from_rest.size} skipped" unless skipped_from_rest.empty?
|
|
110
|
+
|
|
95
111
|
puts
|
|
96
|
-
|
|
97
|
-
|
|
112
|
+
puts "#{total} package(s): #{parts.join(", ")}."
|
|
113
|
+
|
|
114
|
+
Rwm.debug("passed: #{passed.map(&:package_name).join(", ")}") unless passed.empty?
|
|
115
|
+
Rwm.debug("skipped (no matching task): #{skipped_from_rest.map(&:package_name).join(", ")}") unless skipped_from_rest.empty?
|
|
116
|
+
|
|
117
|
+
if failed.empty?
|
|
98
118
|
0
|
|
99
119
|
else
|
|
100
|
-
|
|
101
|
-
$stderr.puts "#{failed.size} package(s) failed:"
|
|
120
|
+
$stderr.puts "Failed:"
|
|
102
121
|
failed.each { |r| $stderr.puts " - #{r.package_name}" }
|
|
103
122
|
1
|
|
104
123
|
end
|
|
@@ -114,6 +133,12 @@ module Rwm
|
|
|
114
133
|
opts.on("--committed", "Only consider committed changes (with --affected)") do
|
|
115
134
|
@committed_only = true
|
|
116
135
|
end
|
|
136
|
+
opts.on("--base REF", "Compare against REF instead of auto-detected base branch (with --affected)") do |ref|
|
|
137
|
+
@base_branch = ref
|
|
138
|
+
end
|
|
139
|
+
opts.on("--dry-run", "Show what would run without executing") do
|
|
140
|
+
@dry_run = true
|
|
141
|
+
end
|
|
117
142
|
opts.on("--no-cache", "Bypass task caching even for cacheable tasks") do
|
|
118
143
|
@no_cache = true
|
|
119
144
|
end
|
data/lib/rwm/dependency_graph.rb
CHANGED
|
@@ -61,7 +61,7 @@ module Rwm
|
|
|
61
61
|
def topological_order
|
|
62
62
|
tsort
|
|
63
63
|
rescue TSort::Cyclic => e
|
|
64
|
-
raise CycleError, [e.message]
|
|
64
|
+
raise CycleError, [[e.message]]
|
|
65
65
|
end
|
|
66
66
|
|
|
67
67
|
# Group packages into execution levels — packages at the same level
|
|
@@ -79,7 +79,7 @@ module Rwm
|
|
|
79
79
|
dependencies(name).all? { |dep| placed.include?(dep) }
|
|
80
80
|
end
|
|
81
81
|
|
|
82
|
-
raise CycleError, ["Unable to resolve execution levels — possible cycle"] if level.empty?
|
|
82
|
+
raise CycleError, [["Unable to resolve execution levels — possible cycle"]] if level.empty?
|
|
83
83
|
|
|
84
84
|
levels << level.sort
|
|
85
85
|
remaining -= level
|
|
@@ -93,13 +93,16 @@ module Rwm
|
|
|
93
93
|
def self.load(workspace)
|
|
94
94
|
path = workspace.graph_path
|
|
95
95
|
unless File.exist?(path)
|
|
96
|
+
Rwm.debug("graph: no cached graph found, building from scratch")
|
|
96
97
|
return build_and_save(workspace)
|
|
97
98
|
end
|
|
98
99
|
|
|
99
100
|
if stale?(path, workspace.packages)
|
|
101
|
+
Rwm.debug("graph: cached graph is stale, rebuilding")
|
|
100
102
|
return build_and_save(workspace)
|
|
101
103
|
end
|
|
102
104
|
|
|
105
|
+
Rwm.debug("graph: loading from cache at #{path}")
|
|
103
106
|
data = JSON.parse(File.read(path))
|
|
104
107
|
graph = new
|
|
105
108
|
|
|
@@ -158,24 +161,7 @@ module Rwm
|
|
|
158
161
|
File.write(path, JSON.pretty_generate(to_json_data) + "\n")
|
|
159
162
|
end
|
|
160
163
|
|
|
161
|
-
def
|
|
162
|
-
data = JSON.parse(File.read(path))
|
|
163
|
-
graph = new
|
|
164
|
-
|
|
165
|
-
# We can't reconstruct full Package objects from JSON alone,
|
|
166
|
-
# but we can load the structure for validation
|
|
167
|
-
data["edges"]&.each do |name, deps|
|
|
168
|
-
graph.instance_variable_get(:@edges)[name] = deps
|
|
169
|
-
deps.each do |dep|
|
|
170
|
-
graph.instance_variable_get(:@dependents)[dep] ||= []
|
|
171
|
-
graph.instance_variable_get(:@dependents)[dep] << name
|
|
172
|
-
end
|
|
173
|
-
end
|
|
174
|
-
|
|
175
|
-
graph
|
|
176
|
-
end
|
|
177
|
-
|
|
178
|
-
def to_dot(workspace_root)
|
|
164
|
+
def to_dot
|
|
179
165
|
lines = []
|
|
180
166
|
lines << "digraph rwm {"
|
|
181
167
|
lines << " rankdir=LR;"
|
|
@@ -195,7 +181,7 @@ module Rwm
|
|
|
195
181
|
lines.join("\n") + "\n"
|
|
196
182
|
end
|
|
197
183
|
|
|
198
|
-
def to_mermaid
|
|
184
|
+
def to_mermaid
|
|
199
185
|
lines = []
|
|
200
186
|
lines << "graph LR"
|
|
201
187
|
|
data/lib/rwm/package.rb
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "pathname"
|
|
4
|
-
require "open3"
|
|
5
4
|
|
|
6
5
|
module Rwm
|
|
7
6
|
class Package
|
|
@@ -25,31 +24,14 @@ module Rwm
|
|
|
25
24
|
File.exist?(File.join(path, "Rakefile"))
|
|
26
25
|
end
|
|
27
26
|
|
|
28
|
-
def has_rake_task?(task)
|
|
29
|
-
return false unless has_rakefile?
|
|
30
|
-
|
|
31
|
-
output, _, status = Open3.capture3("bundle", "exec", "rake", "-P", chdir: path)
|
|
32
|
-
return false unless status.success?
|
|
33
|
-
|
|
34
|
-
output.lines.any? { |line| line.strip == "rake #{task}" }
|
|
35
|
-
end
|
|
36
|
-
|
|
37
27
|
def gemfile_path
|
|
38
28
|
File.join(path, "Gemfile")
|
|
39
29
|
end
|
|
40
30
|
|
|
41
|
-
def gemspec_path
|
|
42
|
-
Dir.glob(File.join(path, "*.gemspec")).first
|
|
43
|
-
end
|
|
44
|
-
|
|
45
31
|
def relative_path(workspace_root)
|
|
46
32
|
Pathname.new(path).relative_path_from(Pathname.new(workspace_root)).to_s
|
|
47
33
|
end
|
|
48
34
|
|
|
49
|
-
def to_s
|
|
50
|
-
"#{name} (#{type})"
|
|
51
|
-
end
|
|
52
|
-
|
|
53
35
|
def ==(other)
|
|
54
36
|
other.is_a?(Package) && name == other.name && path == other.path
|
|
55
37
|
end
|
data/lib/rwm/task_cache.rb
CHANGED
|
@@ -7,6 +7,17 @@ require "open3"
|
|
|
7
7
|
|
|
8
8
|
module Rwm
|
|
9
9
|
class TaskCache
|
|
10
|
+
def self.clean(workspace, package_name: nil)
|
|
11
|
+
cache_dir = File.join(workspace.root, ".rwm", "cache")
|
|
12
|
+
return unless Dir.exist?(cache_dir)
|
|
13
|
+
|
|
14
|
+
if package_name
|
|
15
|
+
Dir.glob(File.join(cache_dir, "#{package_name}-*")).each { |f| File.delete(f) }
|
|
16
|
+
else
|
|
17
|
+
Dir.glob(File.join(cache_dir, "*")).each { |f| File.delete(f) }
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
10
21
|
def initialize(workspace, graph)
|
|
11
22
|
@workspace = workspace
|
|
12
23
|
@graph = graph
|
|
@@ -25,20 +36,33 @@ module Rwm
|
|
|
25
36
|
# Also verifies declared outputs exist (if any).
|
|
26
37
|
def cached?(package, task)
|
|
27
38
|
stored = read_stored_hash(package, task)
|
|
28
|
-
|
|
29
|
-
|
|
39
|
+
unless stored
|
|
40
|
+
Rwm.debug("cache miss: #{package.name}:#{task} (no stored hash)")
|
|
41
|
+
return false
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
current = content_hash(package)
|
|
45
|
+
unless stored == current
|
|
46
|
+
Rwm.debug("cache miss: #{package.name}:#{task} (hash changed)")
|
|
47
|
+
return false
|
|
48
|
+
end
|
|
30
49
|
|
|
31
50
|
# If outputs are declared, they must exist
|
|
32
51
|
decl = cache_declarations(package)[task]
|
|
33
52
|
if decl && decl["output"]
|
|
34
|
-
|
|
53
|
+
unless outputs_exist?(package, decl["output"])
|
|
54
|
+
Rwm.debug("cache miss: #{package.name}:#{task} (outputs missing)")
|
|
55
|
+
return false
|
|
56
|
+
end
|
|
35
57
|
end
|
|
36
58
|
|
|
59
|
+
Rwm.debug("cache hit: #{package.name}:#{task}")
|
|
37
60
|
true
|
|
38
61
|
end
|
|
39
62
|
|
|
40
63
|
# Store the current content hash after a successful task run
|
|
41
64
|
def store(package, task)
|
|
65
|
+
Rwm.debug("cache store: #{package.name}:#{task}")
|
|
42
66
|
FileUtils.mkdir_p(@cache_dir)
|
|
43
67
|
path = cache_file(package, task)
|
|
44
68
|
File.write(path, content_hash(package))
|
|
@@ -78,6 +102,7 @@ module Rwm
|
|
|
78
102
|
def cache_declarations(package)
|
|
79
103
|
return @cache_declarations[package.name] if @cache_declarations.key?(package.name)
|
|
80
104
|
|
|
105
|
+
Rwm.debug("cache declarations: discovering for #{package.name}")
|
|
81
106
|
output, _, status = Open3.capture3("bundle", "exec", "rake", "rwm:cache_config", chdir: package.path)
|
|
82
107
|
@cache_declarations[package.name] = if status.success? && !output.strip.empty?
|
|
83
108
|
JSON.parse(output.strip)
|
|
@@ -91,13 +116,19 @@ module Rwm
|
|
|
91
116
|
private
|
|
92
117
|
|
|
93
118
|
def source_files(package)
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
119
|
+
# Tracked files + untracked-but-not-ignored files (null-delimited for safe filenames)
|
|
120
|
+
output, _, status = Open3.capture3(
|
|
121
|
+
"git", "ls-files", "-z", "--cached", "--others", "--exclude-standard",
|
|
122
|
+
chdir: package.path
|
|
123
|
+
)
|
|
124
|
+
return [] unless status.success?
|
|
125
|
+
|
|
126
|
+
files = output.split("\0")
|
|
127
|
+
.reject(&:empty?)
|
|
128
|
+
.sort
|
|
129
|
+
.map { |f| File.join(package.path, f) }
|
|
130
|
+
Rwm.debug("source_files: #{package.name} → #{files.size} file(s)")
|
|
131
|
+
files
|
|
101
132
|
end
|
|
102
133
|
|
|
103
134
|
def cache_file(package, task)
|
data/lib/rwm/task_runner.rb
CHANGED
|
@@ -5,7 +5,9 @@ require "etc"
|
|
|
5
5
|
|
|
6
6
|
module Rwm
|
|
7
7
|
class TaskRunner
|
|
8
|
-
Result = Struct.new(:package_name, :task, :success, :output, keyword_init: true)
|
|
8
|
+
Result = Struct.new(:package_name, :task, :success, :output, :skipped, keyword_init: true)
|
|
9
|
+
|
|
10
|
+
NO_TASK_PATTERN = /Don't know how to build task/
|
|
9
11
|
|
|
10
12
|
attr_reader :results
|
|
11
13
|
|
|
@@ -28,16 +30,33 @@ module Rwm
|
|
|
28
30
|
completed = Set.new
|
|
29
31
|
skipped = Set.new
|
|
30
32
|
running = {}
|
|
33
|
+
@interrupted = false
|
|
31
34
|
|
|
32
35
|
mutex = Mutex.new
|
|
33
36
|
condition = ConditionVariable.new
|
|
34
37
|
|
|
35
|
-
|
|
38
|
+
previous_trap = Signal.trap("INT") do
|
|
39
|
+
@interrupted = true
|
|
40
|
+
# Cannot use mutex inside trap context — just kill threads directly.
|
|
41
|
+
# Thread#kill is safe to call from trap context.
|
|
42
|
+
running.each_value { |t| t.kill rescue nil }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
done = false
|
|
46
|
+
until done
|
|
47
|
+
break if @interrupted
|
|
48
|
+
|
|
36
49
|
mutex.synchronize do
|
|
50
|
+
if pending.empty? && running.empty?
|
|
51
|
+
done = true
|
|
52
|
+
next
|
|
53
|
+
end
|
|
54
|
+
|
|
37
55
|
ready = pending.select { |pkg| ready?(pkg, package_names, completed) }
|
|
38
56
|
|
|
39
57
|
ready.each do |pkg|
|
|
40
58
|
break if running.size >= @concurrency
|
|
59
|
+
break if @interrupted
|
|
41
60
|
|
|
42
61
|
pending.delete(pkg)
|
|
43
62
|
running[pkg.name] = Thread.new do
|
|
@@ -73,7 +92,11 @@ module Rwm
|
|
|
73
92
|
end
|
|
74
93
|
end
|
|
75
94
|
|
|
95
|
+
raise Interrupt, "Interrupted by Ctrl+C" if @interrupted
|
|
96
|
+
|
|
76
97
|
@results
|
|
98
|
+
ensure
|
|
99
|
+
Signal.trap("INT", previous_trap || "DEFAULT")
|
|
77
100
|
end
|
|
78
101
|
|
|
79
102
|
# Run a rake task in each package
|
|
@@ -105,10 +128,23 @@ module Rwm
|
|
|
105
128
|
def run_single(pkg, &command_proc)
|
|
106
129
|
cmd = command_proc.call(pkg)
|
|
107
130
|
prefix = "[#{pkg.name}]"
|
|
131
|
+
Rwm.debug("running: #{cmd.join(' ')} in #{pkg.path}")
|
|
108
132
|
|
|
109
133
|
stdout, stderr, status = Open3.capture3(*cmd, chdir: pkg.path)
|
|
110
134
|
output = format_output(prefix, stdout, stderr)
|
|
111
135
|
|
|
136
|
+
# Detect "task not found" and treat as skipped, not failed
|
|
137
|
+
if !status.success? && stderr.match?(NO_TASK_PATTERN)
|
|
138
|
+
Rwm.debug("#{pkg.name}: task not found, skipping")
|
|
139
|
+
return Result.new(
|
|
140
|
+
package_name: pkg.name,
|
|
141
|
+
task: cmd.join(" "),
|
|
142
|
+
success: true,
|
|
143
|
+
output: "",
|
|
144
|
+
skipped: true
|
|
145
|
+
)
|
|
146
|
+
end
|
|
147
|
+
|
|
112
148
|
if @buffered
|
|
113
149
|
print_buffered_output(pkg.name, output, status.success?)
|
|
114
150
|
else
|
data/lib/rwm/version.rb
CHANGED
data/lib/rwm/workspace.rb
CHANGED
|
@@ -33,14 +33,6 @@ module Rwm
|
|
|
33
33
|
File.join(rwm_dir, GRAPH_FILE)
|
|
34
34
|
end
|
|
35
35
|
|
|
36
|
-
def libs_dir
|
|
37
|
-
File.join(root, "libs")
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def apps_dir
|
|
41
|
-
File.join(root, "apps")
|
|
42
|
-
end
|
|
43
|
-
|
|
44
36
|
# Discover all packages by scanning libs/ and apps/ for directories with a Gemfile
|
|
45
37
|
def packages
|
|
46
38
|
@packages ||= discover_packages
|
data/lib/rwm.rb
CHANGED
|
@@ -4,6 +4,20 @@ require_relative "rwm/version"
|
|
|
4
4
|
require_relative "rwm/errors"
|
|
5
5
|
|
|
6
6
|
module Rwm
|
|
7
|
+
@verbose = false
|
|
8
|
+
|
|
9
|
+
def self.verbose?
|
|
10
|
+
@verbose
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.verbose=(value)
|
|
14
|
+
@verbose = value
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.debug(msg)
|
|
18
|
+
$stderr.puts "[rwm debug] #{msg}" if @verbose
|
|
19
|
+
end
|
|
20
|
+
|
|
7
21
|
autoload :Workspace, "rwm/workspace"
|
|
8
22
|
autoload :Package, "rwm/package"
|
|
9
23
|
autoload :GemfileParser, "rwm/gemfile_parser"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ruby_workspace_manager
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Siddharth Bhatt
|
|
@@ -20,11 +20,14 @@ files:
|
|
|
20
20
|
- LICENSE.txt
|
|
21
21
|
- README.md
|
|
22
22
|
- bin/rwm
|
|
23
|
+
- completions/rwm.bash
|
|
24
|
+
- completions/rwm.zsh
|
|
23
25
|
- lib/rwm.rb
|
|
24
26
|
- lib/rwm/affected_detector.rb
|
|
25
27
|
- lib/rwm/cli.rb
|
|
26
28
|
- lib/rwm/commands/affected.rb
|
|
27
29
|
- lib/rwm/commands/bootstrap.rb
|
|
30
|
+
- lib/rwm/commands/cache.rb
|
|
28
31
|
- lib/rwm/commands/check.rb
|
|
29
32
|
- lib/rwm/commands/graph.rb
|
|
30
33
|
- lib/rwm/commands/info.rb
|
|
@@ -50,8 +53,8 @@ homepage: https://github.com/sidbhatt11/ruby-workspace-manager
|
|
|
50
53
|
licenses:
|
|
51
54
|
- MIT
|
|
52
55
|
metadata:
|
|
53
|
-
homepage_uri: https://github.com/sidbhatt11/ruby-workspace-manager
|
|
54
56
|
source_code_uri: https://github.com/sidbhatt11/ruby-workspace-manager
|
|
57
|
+
changelog_uri: https://github.com/sidbhatt11/ruby-workspace-manager/blob/main/CHANGELOG.md
|
|
55
58
|
rdoc_options: []
|
|
56
59
|
require_paths:
|
|
57
60
|
- lib
|