carson 3.18.0 → 3.19.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/RELEASE.md +11 -0
- data/VERSION +1 -1
- data/lib/carson/runtime/audit.rb +10 -6
- data/lib/carson/runtime/housekeep.rb +11 -3
- data/lib/carson/runtime/local/onboard.rb +31 -10
- data/lib/carson/runtime/local/sync.rb +4 -0
- data/lib/carson/runtime/local/template.rb +7 -4
- data/lib/carson/runtime/status.rb +28 -13
- data/lib/carson/runtime.rb +64 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 36c68c06630159e0dca3560f62993befe8b50dd324fac5a6f8232895b2a667bd
|
|
4
|
+
data.tar.gz: 1a4462a8b56b6b7fe505705c30ded47e24a279f4fa775baf56b05ae2e48f450a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3f644435f5ca88527edc597a7b19134b701bc970748b5ef075f9b1e3c600b3be60724d80c8567bb919cf378a9a5da2dc54fcc27f29b0da811c0f3880c990f016
|
|
7
|
+
data.tar.gz: 12e3651de47f5f741115c58308c2fe344d542bcc94c3f1234e08990b506b6ffa2c518e5f782ea9cb0a86a699111926c6df1f6571724b5ef3d318666889f2a13f
|
data/RELEASE.md
CHANGED
|
@@ -5,6 +5,17 @@ Release-note scope rule:
|
|
|
5
5
|
- `RELEASE.md` records only version deltas, breaking changes, and migration actions.
|
|
6
6
|
- Operational usage guides live in `MANUAL.md` and `API.md`.
|
|
7
7
|
|
|
8
|
+
## 3.19.0
|
|
9
|
+
|
|
10
|
+
### What changed
|
|
11
|
+
|
|
12
|
+
- **Persistent pending tracking for batch operations** — repos skipped during `--all` operations (due to active worktrees, uncommitted changes, or path errors) are now recorded in `~/.carson/cache/batch_pending.json`. On the next run, pending repos are reported and automatically retried. `carson status --all` shows pending operations per repo with attempt count and timestamp.
|
|
13
|
+
- **Ruby 3.4+ `it` implicit parameter** — single-parameter blocks where the parameter is the receiver now use `it` instead of named block variables (e.g. `.map { it.fetch( :name ) }` instead of `.map { |e| e.fetch( :name ) }`). Guard clause early returns replace `elsif` chains in `remote_sync_status`.
|
|
14
|
+
|
|
15
|
+
### UX improvement
|
|
16
|
+
|
|
17
|
+
- Batch operations are now self-healing: repos that can't be reached on one run aren't silently forgotten. They're logged with reasons and retried on the next invocation. `carson status --all` makes pending operations visible so the user always knows what needs attention.
|
|
18
|
+
|
|
8
19
|
## 3.18.0
|
|
9
20
|
|
|
10
21
|
### What changed
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
1
|
+
3.19.0
|
data/lib/carson/runtime/audit.rb
CHANGED
|
@@ -72,7 +72,7 @@ module Carson
|
|
|
72
72
|
fail_n = checks.fetch( :failing_count )
|
|
73
73
|
pend_n = checks.fetch( :pending_count )
|
|
74
74
|
total = checks.fetch( :required_total )
|
|
75
|
-
fail_names = checks.fetch( :failing ).map {
|
|
75
|
+
fail_names = checks.fetch( :failing ).map { it.fetch( :name ) }.join( ", " )
|
|
76
76
|
if fail_n.positive? && pend_n.positive?
|
|
77
77
|
audit_concise_problems << "Checks: #{fail_n} failing (#{fail_names}), #{pend_n} pending of #{total} required."
|
|
78
78
|
elsif fail_n.positive?
|
|
@@ -89,11 +89,11 @@ module Carson
|
|
|
89
89
|
if baseline_st == "block"
|
|
90
90
|
parts = []
|
|
91
91
|
if default_branch_baseline.fetch( :failing_count ).positive?
|
|
92
|
-
names = default_branch_baseline.fetch( :failing ).map {
|
|
92
|
+
names = default_branch_baseline.fetch( :failing ).map { it.fetch( :name ) }.join( ", " )
|
|
93
93
|
parts << "#{default_branch_baseline.fetch( :failing_count )} failing (#{names})"
|
|
94
94
|
end
|
|
95
95
|
if default_branch_baseline.fetch( :pending_count ).positive?
|
|
96
|
-
names = default_branch_baseline.fetch( :pending ).map {
|
|
96
|
+
names = default_branch_baseline.fetch( :pending ).map { it.fetch( :name ) }.join( ", " )
|
|
97
97
|
parts << "#{default_branch_baseline.fetch( :pending_count )} pending (#{names})"
|
|
98
98
|
end
|
|
99
99
|
parts << "no check-runs for active workflows" if default_branch_baseline.fetch( :no_check_evidence )
|
|
@@ -101,11 +101,11 @@ module Carson
|
|
|
101
101
|
elsif baseline_st == "attention"
|
|
102
102
|
parts = []
|
|
103
103
|
if default_branch_baseline.fetch( :advisory_failing_count ).positive?
|
|
104
|
-
names = default_branch_baseline.fetch( :advisory_failing ).map {
|
|
104
|
+
names = default_branch_baseline.fetch( :advisory_failing ).map { it.fetch( :name ) }.join( ", " )
|
|
105
105
|
parts << "#{default_branch_baseline.fetch( :advisory_failing_count )} advisory failing (#{names})"
|
|
106
106
|
end
|
|
107
107
|
if default_branch_baseline.fetch( :advisory_pending_count ).positive?
|
|
108
|
-
names = default_branch_baseline.fetch( :advisory_pending ).map {
|
|
108
|
+
names = default_branch_baseline.fetch( :advisory_pending ).map { it.fetch( :name ) }.join( ", " )
|
|
109
109
|
parts << "#{default_branch_baseline.fetch( :advisory_pending_count )} advisory pending (#{names})"
|
|
110
110
|
end
|
|
111
111
|
audit_concise_problems << "Baseline (#{default_branch_baseline.fetch( :default_branch, config.main_branch )}): #{parts.join( ', ' )}."
|
|
@@ -176,6 +176,7 @@ module Carson
|
|
|
176
176
|
repo_name = File.basename( repo_path )
|
|
177
177
|
unless Dir.exist?( repo_path )
|
|
178
178
|
puts_line "#{repo_name}: FAIL (path not found)"
|
|
179
|
+
record_batch_skip( command: "audit", repo_path: repo_path, reason: "path not found" )
|
|
179
180
|
failed += 1
|
|
180
181
|
next
|
|
181
182
|
end
|
|
@@ -186,16 +187,19 @@ module Carson
|
|
|
186
187
|
case status
|
|
187
188
|
when EXIT_OK
|
|
188
189
|
puts_line "#{repo_name}: ok" unless verbose?
|
|
190
|
+
clear_batch_success( command: "audit", repo_path: repo_path )
|
|
189
191
|
passed += 1
|
|
190
192
|
when EXIT_BLOCK
|
|
191
193
|
puts_line "#{repo_name}: BLOCK" unless verbose?
|
|
192
194
|
blocked += 1
|
|
193
195
|
else
|
|
194
196
|
puts_line "#{repo_name}: FAIL" unless verbose?
|
|
197
|
+
record_batch_skip( command: "audit", repo_path: repo_path, reason: "audit failed" )
|
|
195
198
|
failed += 1
|
|
196
199
|
end
|
|
197
200
|
rescue StandardError => e
|
|
198
201
|
puts_line "#{repo_name}: FAIL (#{e.message})"
|
|
202
|
+
record_batch_skip( command: "audit", repo_path: repo_path, reason: e.message )
|
|
199
203
|
failed += 1
|
|
200
204
|
end
|
|
201
205
|
end
|
|
@@ -258,7 +262,7 @@ module Carson
|
|
|
258
262
|
return report
|
|
259
263
|
end
|
|
260
264
|
checks_data = JSON.parse( checks_stdout )
|
|
261
|
-
pending = checks_data.select {
|
|
265
|
+
pending = checks_data.select { it[ "bucket" ].to_s == "pending" }
|
|
262
266
|
failing = checks_data.select { |entry| check_entry_failing?( entry: entry ) }
|
|
263
267
|
report[ :checks ][ :status ] = checks_success ? "ok" : ( checks_exit == 8 ? "pending" : "attention" )
|
|
264
268
|
report[ :checks ][ :required_total ] = checks_data.count
|
|
@@ -34,10 +34,18 @@ module Carson
|
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
results = []
|
|
37
|
-
repos.each
|
|
37
|
+
repos.each do |repo_path|
|
|
38
|
+
entry = housekeep_one_entry( repo_path: repo_path, silent: json_output )
|
|
39
|
+
if entry[ :status ] == "ok"
|
|
40
|
+
clear_batch_success( command: "housekeep", repo_path: repo_path )
|
|
41
|
+
else
|
|
42
|
+
record_batch_skip( command: "housekeep", repo_path: repo_path, reason: entry[ :error ] || "housekeep failed" )
|
|
43
|
+
end
|
|
44
|
+
results << entry
|
|
45
|
+
end
|
|
38
46
|
|
|
39
|
-
succeeded = results.count {
|
|
40
|
-
failed = results.count {
|
|
47
|
+
succeeded = results.count { it[ :status ] == "ok" }
|
|
48
|
+
failed = results.count { it[ :status ] != "ok" }
|
|
41
49
|
result = { command: "housekeep", status: failed.zero? ? "ok" : "partial", repos: results, succeeded: succeeded, failed: failed }
|
|
42
50
|
housekeep_finish( result: result, exit_code: failed.zero? ? EXIT_OK : EXIT_ERROR, json_output: json_output, results: results, succeeded: succeeded, failed: failed )
|
|
43
51
|
end
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
# Repository onboarding and refresh lifecycle.
|
|
2
|
+
# Onboard: detect remote, install hooks, apply templates, run initial audit.
|
|
3
|
+
# Refresh: re-apply hooks and templates after Carson upgrade.
|
|
4
|
+
# Refresh all: batch refresh across governed portfolio with safety checks.
|
|
1
5
|
module Carson
|
|
2
6
|
class Runtime
|
|
3
7
|
module Local
|
|
@@ -44,7 +48,7 @@ module Carson
|
|
|
44
48
|
hook_status = prepare!
|
|
45
49
|
return hook_status unless hook_status == EXIT_OK
|
|
46
50
|
|
|
47
|
-
drift_count = template_results.count {
|
|
51
|
+
drift_count = template_results.count { it.fetch( :status ) != "ok" }
|
|
48
52
|
template_status = template_apply!
|
|
49
53
|
return template_status unless template_status == EXIT_OK
|
|
50
54
|
|
|
@@ -64,7 +68,7 @@ module Carson
|
|
|
64
68
|
return hook_status unless hook_status == EXIT_OK
|
|
65
69
|
puts_line "Hooks installed (#{config.managed_hooks.count} hooks)."
|
|
66
70
|
|
|
67
|
-
template_drift_count = template_results.count {
|
|
71
|
+
template_drift_count = template_results.count { it.fetch( :status ) != "ok" }
|
|
68
72
|
template_status = with_captured_output { template_apply! }
|
|
69
73
|
return template_status unless template_status == EXIT_OK
|
|
70
74
|
if template_drift_count.positive?
|
|
@@ -82,7 +86,7 @@ module Carson
|
|
|
82
86
|
|
|
83
87
|
# Re-applies hooks, templates, and audit across all governed repositories.
|
|
84
88
|
# Checks each repo for safety (active worktrees, uncommitted changes) and
|
|
85
|
-
#
|
|
89
|
+
# marks unsafe repos as pending to avoid disrupting active work.
|
|
86
90
|
def refresh_all!
|
|
87
91
|
repos = config.govern_repos
|
|
88
92
|
if repos.empty?
|
|
@@ -91,24 +95,32 @@ module Carson
|
|
|
91
95
|
return EXIT_ERROR
|
|
92
96
|
end
|
|
93
97
|
|
|
98
|
+
pending_before = pending_repos_for( command: "refresh" )
|
|
99
|
+
if pending_before.any?
|
|
100
|
+
puts_line "#{pending_before.length} repo#{plural_suffix( count: pending_before.length )} pending from previous run"
|
|
101
|
+
end
|
|
102
|
+
|
|
94
103
|
puts_line ""
|
|
95
104
|
puts_line "Refresh all (#{repos.length} repo#{plural_suffix( count: repos.length )})"
|
|
96
105
|
refreshed = 0
|
|
97
|
-
|
|
106
|
+
pending = 0
|
|
98
107
|
failed = 0
|
|
99
108
|
|
|
100
109
|
repos.each do |repo_path|
|
|
101
110
|
repo_name = File.basename( repo_path )
|
|
102
111
|
unless Dir.exist?( repo_path )
|
|
103
112
|
puts_line "#{repo_name}: FAIL (path not found)"
|
|
113
|
+
record_batch_skip( command: "refresh", repo_path: repo_path, reason: "path not found" )
|
|
104
114
|
failed += 1
|
|
105
115
|
next
|
|
106
116
|
end
|
|
107
117
|
|
|
108
118
|
safety = portfolio_repo_safety( repo_path: repo_path )
|
|
109
119
|
unless safety.fetch( :safe )
|
|
110
|
-
|
|
111
|
-
|
|
120
|
+
reason = safety.fetch( :reasons ).join( ", " )
|
|
121
|
+
puts_line "#{repo_name}: PENDING (#{reason})"
|
|
122
|
+
record_batch_skip( command: "refresh", repo_path: repo_path, reason: reason )
|
|
123
|
+
pending += 1
|
|
112
124
|
next
|
|
113
125
|
end
|
|
114
126
|
|
|
@@ -116,16 +128,17 @@ module Carson
|
|
|
116
128
|
if status == EXIT_ERROR
|
|
117
129
|
failed += 1
|
|
118
130
|
else
|
|
131
|
+
clear_batch_success( command: "refresh", repo_path: repo_path )
|
|
119
132
|
refreshed += 1
|
|
120
133
|
end
|
|
121
134
|
end
|
|
122
135
|
|
|
123
136
|
puts_line ""
|
|
124
137
|
parts = [ "#{refreshed} refreshed" ]
|
|
125
|
-
parts << "#{
|
|
138
|
+
parts << "#{pending} still pending (will retry on next run)" if pending.positive?
|
|
126
139
|
parts << "#{failed} failed" if failed.positive?
|
|
127
140
|
puts_line "Refresh all complete: #{parts.join( ', ' )}."
|
|
128
|
-
failed.zero? &&
|
|
141
|
+
failed.zero? && pending.zero? ? EXIT_OK : EXIT_ERROR
|
|
129
142
|
end
|
|
130
143
|
|
|
131
144
|
def prune_all!
|
|
@@ -145,6 +158,7 @@ module Carson
|
|
|
145
158
|
repo_name = File.basename( repo_path )
|
|
146
159
|
unless Dir.exist?( repo_path )
|
|
147
160
|
puts_line "#{repo_name}: FAIL (path not found)"
|
|
161
|
+
record_batch_skip( command: "prune", repo_path: repo_path, reason: "path not found" )
|
|
148
162
|
failed += 1
|
|
149
163
|
next
|
|
150
164
|
end
|
|
@@ -158,9 +172,16 @@ module Carson
|
|
|
158
172
|
summary = buf.string.lines.last.to_s.strip
|
|
159
173
|
puts_line "#{repo_name}: #{summary.empty? ? 'OK' : summary}"
|
|
160
174
|
end
|
|
161
|
-
status == EXIT_ERROR
|
|
175
|
+
if status == EXIT_ERROR
|
|
176
|
+
record_batch_skip( command: "prune", repo_path: repo_path, reason: "prune failed" )
|
|
177
|
+
failed += 1
|
|
178
|
+
else
|
|
179
|
+
clear_batch_success( command: "prune", repo_path: repo_path )
|
|
180
|
+
succeeded += 1
|
|
181
|
+
end
|
|
162
182
|
rescue StandardError => e
|
|
163
183
|
puts_line "#{repo_name}: FAIL (#{e.message})"
|
|
184
|
+
record_batch_skip( command: "prune", repo_path: repo_path, reason: e.message )
|
|
164
185
|
failed += 1
|
|
165
186
|
end
|
|
166
187
|
end
|
|
@@ -227,7 +248,7 @@ module Carson
|
|
|
227
248
|
return hook_status unless hook_status == EXIT_OK
|
|
228
249
|
puts_line "Hooks installed (#{config.managed_hooks.count} hooks)."
|
|
229
250
|
|
|
230
|
-
template_drift_count = template_results.count {
|
|
251
|
+
template_drift_count = template_results.count { it.fetch( :status ) != "ok" }
|
|
231
252
|
template_status = with_captured_output { template_apply! }
|
|
232
253
|
return template_status unless template_status == EXIT_OK
|
|
233
254
|
if template_drift_count.positive?
|
|
@@ -60,6 +60,7 @@ module Carson
|
|
|
60
60
|
repo_name = File.basename( repo_path )
|
|
61
61
|
unless Dir.exist?( repo_path )
|
|
62
62
|
puts_line "#{repo_name}: FAIL (path not found)"
|
|
63
|
+
record_batch_skip( command: "sync", repo_path: repo_path, reason: "path not found" )
|
|
63
64
|
failed += 1
|
|
64
65
|
next
|
|
65
66
|
end
|
|
@@ -69,13 +70,16 @@ module Carson
|
|
|
69
70
|
status = rt.sync!
|
|
70
71
|
if status == EXIT_OK
|
|
71
72
|
puts_line "#{repo_name}: ok" unless verbose?
|
|
73
|
+
clear_batch_success( command: "sync", repo_path: repo_path )
|
|
72
74
|
synced += 1
|
|
73
75
|
else
|
|
74
76
|
puts_line "#{repo_name}: FAIL" unless verbose?
|
|
77
|
+
record_batch_skip( command: "sync", repo_path: repo_path, reason: "sync failed" )
|
|
75
78
|
failed += 1
|
|
76
79
|
end
|
|
77
80
|
rescue StandardError => e
|
|
78
81
|
puts_line "#{repo_name}: FAIL (#{e.message})"
|
|
82
|
+
record_batch_skip( command: "sync", repo_path: repo_path, reason: e.message )
|
|
79
83
|
failed += 1
|
|
80
84
|
end
|
|
81
85
|
end
|
|
@@ -18,8 +18,8 @@ module Carson
|
|
|
18
18
|
puts_verbose "[Template Sync Check]"
|
|
19
19
|
results = template_results
|
|
20
20
|
stale = template_superseded_present
|
|
21
|
-
drift_count = results.count {
|
|
22
|
-
error_count = results.count {
|
|
21
|
+
drift_count = results.count { it.fetch( :status ) == "drift" }
|
|
22
|
+
error_count = results.count { it.fetch( :status ) == "error" }
|
|
23
23
|
stale_count = stale.count
|
|
24
24
|
results.each do |entry|
|
|
25
25
|
puts_verbose "template_file: #{entry.fetch( :file )} status=#{entry.fetch( :status )} reason=#{entry.fetch( :reason )}"
|
|
@@ -32,7 +32,7 @@ module Carson
|
|
|
32
32
|
summary_parts << "#{drift_count} of #{results.count} drifted" if drift_count.positive?
|
|
33
33
|
summary_parts << "#{stale_count} stale" if stale_count.positive?
|
|
34
34
|
puts_line "Templates: #{summary_parts.join( ", " )}"
|
|
35
|
-
results.select {
|
|
35
|
+
results.select { it.fetch( :status ) == "drift" }.each { |entry| puts_line " #{entry.fetch( :file )}" }
|
|
36
36
|
stale.each { |file| puts_line " #{file} — superseded" }
|
|
37
37
|
else
|
|
38
38
|
puts_line "Templates: #{results.count} files in sync"
|
|
@@ -62,6 +62,7 @@ module Carson
|
|
|
62
62
|
repo_name = File.basename( repo_path )
|
|
63
63
|
unless Dir.exist?( repo_path )
|
|
64
64
|
puts_line "#{repo_name}: FAIL (path not found)"
|
|
65
|
+
record_batch_skip( command: "template_check", repo_path: repo_path, reason: "path not found" )
|
|
65
66
|
failed += 1
|
|
66
67
|
next
|
|
67
68
|
end
|
|
@@ -71,6 +72,7 @@ module Carson
|
|
|
71
72
|
status = rt.template_check!
|
|
72
73
|
if status == EXIT_OK
|
|
73
74
|
puts_line "#{repo_name}: in sync" unless verbose?
|
|
75
|
+
clear_batch_success( command: "template_check", repo_path: repo_path )
|
|
74
76
|
in_sync += 1
|
|
75
77
|
else
|
|
76
78
|
puts_line "#{repo_name}: DRIFT" unless verbose?
|
|
@@ -78,6 +80,7 @@ module Carson
|
|
|
78
80
|
end
|
|
79
81
|
rescue StandardError => e
|
|
80
82
|
puts_line "#{repo_name}: FAIL (#{e.message})"
|
|
83
|
+
record_batch_skip( command: "template_check", repo_path: repo_path, reason: e.message )
|
|
81
84
|
failed += 1
|
|
82
85
|
end
|
|
83
86
|
end
|
|
@@ -124,7 +127,7 @@ module Carson
|
|
|
124
127
|
removed += 1
|
|
125
128
|
end
|
|
126
129
|
|
|
127
|
-
error_count = results.count {
|
|
130
|
+
error_count = results.count { it.fetch( :status ) == "error" }
|
|
128
131
|
puts_verbose "template_apply_summary: updated=#{applied} removed=#{removed} error=#{error_count}"
|
|
129
132
|
unless verbose?
|
|
130
133
|
if applied.positive? || removed.positive?
|
|
@@ -49,6 +49,7 @@ module Carson
|
|
|
49
49
|
puts_line "Carson #{Carson::VERSION} — Portfolio (#{repos.length} repo#{plural_suffix( count: repos.length )})"
|
|
50
50
|
puts_line ""
|
|
51
51
|
|
|
52
|
+
all_pending = load_batch_pending
|
|
52
53
|
repos.each do |repo_path|
|
|
53
54
|
repo_name = File.basename( repo_path )
|
|
54
55
|
unless Dir.exist?( repo_path )
|
|
@@ -68,6 +69,10 @@ module Carson
|
|
|
68
69
|
parts << "#{worktrees.count} worktree#{plural_suffix( count: worktrees.count )}" if worktrees.any?
|
|
69
70
|
parts << "templates #{gov.fetch( :templates )}" unless gov.fetch( :templates ) == :in_sync
|
|
70
71
|
puts_line "#{repo_name}: #{parts.join( ' ' )}"
|
|
72
|
+
|
|
73
|
+
# Show pending operations for this repo.
|
|
74
|
+
repo_pending = status_pending_for_repo( all_pending: all_pending, repo_path: repo_path )
|
|
75
|
+
repo_pending.each { |desc| puts_line " pending: #{desc}" }
|
|
71
76
|
rescue StandardError => e
|
|
72
77
|
puts_line "#{repo_name}: FAIL (#{e.message})"
|
|
73
78
|
end
|
|
@@ -78,6 +83,21 @@ module Carson
|
|
|
78
83
|
|
|
79
84
|
private
|
|
80
85
|
|
|
86
|
+
# Returns an array of human-readable pending descriptions for a repo.
|
|
87
|
+
def status_pending_for_repo( all_pending:, repo_path: )
|
|
88
|
+
descriptions = []
|
|
89
|
+
all_pending.each do |command, repos|
|
|
90
|
+
next unless repos.is_a?( Hash ) && repos.key?( repo_path )
|
|
91
|
+
|
|
92
|
+
info = repos[ repo_path ]
|
|
93
|
+
attempts = info.fetch( "attempts", 0 )
|
|
94
|
+
skipped_at = info.fetch( "skipped_at", nil )
|
|
95
|
+
time_part = skipped_at ? ", since #{skipped_at[ 11..15 ]}" : ""
|
|
96
|
+
descriptions << "#{command} (#{attempts} attempt#{attempts == 1 ? '' : 's'}#{time_part})"
|
|
97
|
+
end
|
|
98
|
+
descriptions
|
|
99
|
+
end
|
|
100
|
+
|
|
81
101
|
# Collects all status facets into a structured hash.
|
|
82
102
|
def gather_status
|
|
83
103
|
data = {
|
|
@@ -129,15 +149,10 @@ module Carson
|
|
|
129
149
|
ahead = parts[ 0 ].to_i
|
|
130
150
|
behind = parts[ 1 ].to_i
|
|
131
151
|
|
|
132
|
-
if ahead.zero? && behind.zero?
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
elsif ahead.zero? && behind.positive?
|
|
137
|
-
:behind
|
|
138
|
-
else
|
|
139
|
-
:diverged
|
|
140
|
-
end
|
|
152
|
+
return :in_sync if ahead.zero? && behind.zero?
|
|
153
|
+
return :ahead if behind.zero?
|
|
154
|
+
return :behind if ahead.zero?
|
|
155
|
+
:diverged
|
|
141
156
|
end
|
|
142
157
|
|
|
143
158
|
# Lists all worktrees with branch name.
|
|
@@ -147,7 +162,7 @@ module Carson
|
|
|
147
162
|
# Filter out the main worktree (the repository root itself).
|
|
148
163
|
# Use realpath for comparison — git returns canonical paths that may differ from repo_root.
|
|
149
164
|
canonical_root = realpath_safe( repo_root )
|
|
150
|
-
entries.reject {
|
|
165
|
+
entries.reject { it.fetch( :path ) == canonical_root }.map do |wt|
|
|
151
166
|
{
|
|
152
167
|
path: wt.fetch( :path ),
|
|
153
168
|
name: File.basename( wt.fetch( :path ) ),
|
|
@@ -185,9 +200,9 @@ module Carson
|
|
|
185
200
|
entries = Array( rollup )
|
|
186
201
|
return :none if entries.empty?
|
|
187
202
|
|
|
188
|
-
states = entries.map {
|
|
189
|
-
return :fail if states.any? {
|
|
190
|
-
return :pending if states.any? {
|
|
203
|
+
states = entries.map { it[ "conclusion" ].to_s.upcase }
|
|
204
|
+
return :fail if states.any? { it == "FAILURE" || it == "CANCELLED" || it == "TIMED_OUT" }
|
|
205
|
+
return :pending if states.any? { it == "" || it == "PENDING" || it == "QUEUED" || it == "IN_PROGRESS" }
|
|
191
206
|
|
|
192
207
|
:pass
|
|
193
208
|
end
|
data/lib/carson/runtime.rb
CHANGED
|
@@ -213,6 +213,70 @@ module Carson
|
|
|
213
213
|
github_adapter.run( *args )
|
|
214
214
|
end
|
|
215
215
|
|
|
216
|
+
# --- Batch pending tracking (shared by all --all commands) ---
|
|
217
|
+
|
|
218
|
+
# Path to the persistent pending log for batch operations.
|
|
219
|
+
def batch_pending_path
|
|
220
|
+
File.join( report_dir_path, "batch_pending.json" )
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Reads and parses the pending log. Returns empty hash if missing or corrupt.
|
|
224
|
+
def load_batch_pending
|
|
225
|
+
path = batch_pending_path
|
|
226
|
+
return {} unless File.file?( path )
|
|
227
|
+
|
|
228
|
+
JSON.parse( File.read( path ) )
|
|
229
|
+
rescue StandardError
|
|
230
|
+
{}
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Writes the pending log atomically.
|
|
234
|
+
def save_batch_pending( data )
|
|
235
|
+
path = batch_pending_path
|
|
236
|
+
FileUtils.mkdir_p( File.dirname( path ) )
|
|
237
|
+
tmp = "#{path}.tmp"
|
|
238
|
+
File.write( tmp, JSON.pretty_generate( data ) )
|
|
239
|
+
File.rename( tmp, path )
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Adds or updates an entry in the pending log, incrementing attempts.
|
|
243
|
+
def record_batch_skip( command:, repo_path:, reason: )
|
|
244
|
+
data = load_batch_pending
|
|
245
|
+
data[ command ] ||= {}
|
|
246
|
+
existing = data[ command ][ repo_path ]
|
|
247
|
+
attempts = existing ? existing.fetch( "attempts", 0 ) + 1 : 1
|
|
248
|
+
data[ command ][ repo_path ] = {
|
|
249
|
+
"skipped_at" => Time.now.utc.iso8601,
|
|
250
|
+
"reason" => reason,
|
|
251
|
+
"attempts" => attempts
|
|
252
|
+
}
|
|
253
|
+
save_batch_pending( data )
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Removes an entry from the pending log after successful completion.
|
|
257
|
+
def clear_batch_success( command:, repo_path: )
|
|
258
|
+
data = load_batch_pending
|
|
259
|
+
return unless data.key?( command )
|
|
260
|
+
|
|
261
|
+
data[ command ].delete( repo_path )
|
|
262
|
+
data.delete( command ) if data[ command ].empty?
|
|
263
|
+
save_batch_pending( data )
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Returns array of pending repo info hashes for a command.
|
|
267
|
+
def pending_repos_for( command: )
|
|
268
|
+
data = load_batch_pending
|
|
269
|
+
entries = data.fetch( command, {} )
|
|
270
|
+
entries.map do |path, info|
|
|
271
|
+
{
|
|
272
|
+
path: path,
|
|
273
|
+
reason: info.fetch( "reason", "unknown" ),
|
|
274
|
+
skipped_at: info.fetch( "skipped_at", nil ),
|
|
275
|
+
attempts: info.fetch( "attempts", 0 )
|
|
276
|
+
}
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
|
|
216
280
|
# --- Portfolio helpers (shared by all --all commands) ---
|
|
217
281
|
|
|
218
282
|
# Checks whether a governed repo is safe for batch operations.
|