carson 3.30.2 → 3.30.3
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/API.md +16 -27
- data/MANUAL.md +19 -30
- data/README.md +5 -5
- data/RELEASE.md +23 -0
- data/VERSION +1 -1
- data/carson.gemspec +1 -1
- data/lib/carson/cli.rb +298 -225
- data/lib/carson/runtime/audit.rb +0 -52
- data/lib/carson/runtime/deliver.rb +1 -1
- data/lib/carson/runtime/housekeep.rb +0 -78
- data/lib/carson/runtime/{repos.rb → list.rb} +4 -4
- data/lib/carson/runtime/local/hooks.rb +1 -1
- data/lib/carson/runtime/local/onboard.rb +0 -50
- data/lib/carson/runtime/local/sync.rb +1 -48
- data/lib/carson/runtime/local/template.rb +0 -47
- data/lib/carson/runtime/{govern.rb → receive.rb} +37 -46
- data/lib/carson/runtime/recover.rb +2 -2
- data/lib/carson/runtime/status.rb +1 -47
- data/lib/carson/runtime.rb +11 -7
- metadata +8 -8
- /data/{hooks → config/.github/hooks}/command-guard +0 -0
- /data/{hooks → config/.github/hooks}/pre-commit +0 -0
- /data/{hooks → config/.github/hooks}/pre-merge-commit +0 -0
- /data/{hooks → config/.github/hooks}/pre-push +0 -0
- /data/{hooks → config/.github/hooks}/prepare-commit-msg +0 -0
data/lib/carson/runtime/audit.rb
CHANGED
|
@@ -164,58 +164,6 @@ module Carson
|
|
|
164
164
|
exit_code
|
|
165
165
|
end
|
|
166
166
|
|
|
167
|
-
# Runs audit across all governed repositories.
|
|
168
|
-
def audit_all!
|
|
169
|
-
repos = config.govern_repos
|
|
170
|
-
if repos.empty?
|
|
171
|
-
puts_line "No governed repositories configured."
|
|
172
|
-
puts_line " Run carson onboard in each repo to register."
|
|
173
|
-
return EXIT_ERROR
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
puts_line ""
|
|
177
|
-
puts_line "Audit all (#{repos.length} repo#{plural_suffix( count: repos.length )})"
|
|
178
|
-
passed = 0
|
|
179
|
-
blocked = 0
|
|
180
|
-
failed = 0
|
|
181
|
-
|
|
182
|
-
repos.each do |repo_path|
|
|
183
|
-
repo_name = File.basename( repo_path )
|
|
184
|
-
unless Dir.exist?( repo_path )
|
|
185
|
-
puts_line "#{repo_name}: not found"
|
|
186
|
-
record_batch_skip( command: "audit", repo_path: repo_path, reason: "path not found" )
|
|
187
|
-
failed += 1
|
|
188
|
-
next
|
|
189
|
-
end
|
|
190
|
-
|
|
191
|
-
begin
|
|
192
|
-
scoped_runtime = build_scoped_runtime( repo_path: repo_path )
|
|
193
|
-
status = scoped_runtime.audit!
|
|
194
|
-
case status
|
|
195
|
-
when EXIT_OK
|
|
196
|
-
puts_line "#{repo_name}: ok" unless verbose?
|
|
197
|
-
clear_batch_success( command: "audit", repo_path: repo_path )
|
|
198
|
-
passed += 1
|
|
199
|
-
when EXIT_BLOCK
|
|
200
|
-
puts_line "#{repo_name}: needs attention" unless verbose?
|
|
201
|
-
blocked += 1
|
|
202
|
-
else
|
|
203
|
-
puts_line "#{repo_name}: could not complete" unless verbose?
|
|
204
|
-
record_batch_skip( command: "audit", repo_path: repo_path, reason: "audit failed" )
|
|
205
|
-
failed += 1
|
|
206
|
-
end
|
|
207
|
-
rescue StandardError => exception
|
|
208
|
-
puts_line "#{repo_name}: could not complete (#{exception.message})"
|
|
209
|
-
record_batch_skip( command: "audit", repo_path: repo_path, reason: exception.message )
|
|
210
|
-
failed += 1
|
|
211
|
-
end
|
|
212
|
-
end
|
|
213
|
-
|
|
214
|
-
puts_line ""
|
|
215
|
-
puts_line "Audit all complete: #{passed} ok, #{blocked} blocked, #{failed} failed."
|
|
216
|
-
blocked.zero? && failed.zero? ? EXIT_OK : EXIT_BLOCK
|
|
217
|
-
end
|
|
218
|
-
|
|
219
167
|
# rubocop:disable Layout/AccessModifierIndentation -- tab-width calculation produces unfixable mixed tabs+spaces
|
|
220
168
|
private
|
|
221
169
|
# rubocop:enable Layout/AccessModifierIndentation
|
|
@@ -23,73 +23,6 @@ module Carson
|
|
|
23
23
|
housekeep_one( repo_path: canonical, json_output: json_output )
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
-
# Resolves a target name to a governed repo, then serves it.
|
|
27
|
-
def housekeep_target!( target:, json_output: false, dry_run: false )
|
|
28
|
-
repo_path = resolve_governed_repo( target: target )
|
|
29
|
-
unless repo_path
|
|
30
|
-
result = { command: "housekeep", status: "error", error: "Not a governed repository: #{target}", recovery: "Run carson repos to see governed repositories." }
|
|
31
|
-
return housekeep_finish( result: result, exit_code: EXIT_ERROR, json_output: json_output )
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
if dry_run
|
|
35
|
-
scoped = Runtime.new( repo_root: repo_path, tool_root: tool_root, output: output, error: error, verbose: verbose? )
|
|
36
|
-
return scoped.housekeep_one_dry_run
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
housekeep_one( repo_path: repo_path, json_output: json_output )
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
# Knocks each governed repo's gate in turn.
|
|
43
|
-
def housekeep_all!( json_output: false, dry_run: false )
|
|
44
|
-
repos = config.govern_repos
|
|
45
|
-
if repos.empty?
|
|
46
|
-
result = { command: "housekeep", status: "error", error: "No governed repositories configured.", recovery: "Run carson onboard in each repo to register." }
|
|
47
|
-
return housekeep_finish( result: result, exit_code: EXIT_ERROR, json_output: json_output )
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
if dry_run
|
|
51
|
-
repos.each_with_index do |repo_path, idx|
|
|
52
|
-
puts_line "" if idx > 0
|
|
53
|
-
unless Dir.exist?( repo_path )
|
|
54
|
-
puts_line "#{File.basename( repo_path )}: SKIP (path not found)"
|
|
55
|
-
next
|
|
56
|
-
end
|
|
57
|
-
scoped = Runtime.new( repo_root: repo_path, tool_root: tool_root, output: output, error: error, verbose: verbose? )
|
|
58
|
-
scoped.housekeep_one_dry_run
|
|
59
|
-
end
|
|
60
|
-
total = repos.size
|
|
61
|
-
puts_line ""
|
|
62
|
-
puts_line "#{total} repo#{plural_suffix( count: total )} surveyed. Run without --dry-run to apply."
|
|
63
|
-
return EXIT_OK
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
results = []
|
|
67
|
-
repos.each do |repo_path|
|
|
68
|
-
entry = housekeep_one_entry( repo_path: repo_path, silent: json_output )
|
|
69
|
-
if entry[ :status ] == "ok"
|
|
70
|
-
clear_batch_success( command: "housekeep", repo_path: repo_path )
|
|
71
|
-
else
|
|
72
|
-
record_batch_skip( command: "housekeep", repo_path: repo_path, reason: entry[ :error ] || "housekeep failed" )
|
|
73
|
-
end
|
|
74
|
-
results << entry
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
succeeded = results.count { |entry| entry[ :status ] == "ok" }
|
|
78
|
-
failed = results.count { |entry| entry[ :status ] != "ok" }
|
|
79
|
-
result = { command: "housekeep", status: failed.zero? ? "ok" : "partial", repos: results, succeeded: succeeded, failed: failed }
|
|
80
|
-
housekeep_finish( result: result, exit_code: failed.zero? ? EXIT_OK : EXIT_ERROR, json_output: json_output, results: results, succeeded: succeeded, failed: failed )
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
def housekeep_loop!( json_output:, dry_run:, loop_seconds: )
|
|
84
|
-
run_signal_aware_loop!(
|
|
85
|
-
loop_name: "housekeep",
|
|
86
|
-
loop_seconds: loop_seconds,
|
|
87
|
-
cycle_line: ->( cycle_count ) { "housekeep cycle #{cycle_count} at #{Time.now.utc.strftime( '%Y-%m-%d %H:%M:%S UTC' )}" }
|
|
88
|
-
) do
|
|
89
|
-
housekeep_all!( json_output: json_output, dry_run: dry_run )
|
|
90
|
-
end
|
|
91
|
-
end
|
|
92
|
-
|
|
93
26
|
# Prints a dry-run plan for this repo without making any changes.
|
|
94
27
|
# Calls reap_dead_worktrees_plan and prune_plan on self (already scoped to the repo).
|
|
95
28
|
def housekeep_one_dry_run
|
|
@@ -185,17 +118,6 @@ module Carson
|
|
|
185
118
|
{ name: repo_name, path: repo_path, status: "error", error: exception.message }
|
|
186
119
|
end
|
|
187
120
|
|
|
188
|
-
# Resolves a user-supplied target to a governed repository path.
|
|
189
|
-
# Accepts: exact path, expandable path, or basename match (case-insensitive).
|
|
190
|
-
def resolve_governed_repo( target: )
|
|
191
|
-
repos = config.govern_repos
|
|
192
|
-
expanded = File.expand_path( target )
|
|
193
|
-
return expanded if repos.include?( expanded )
|
|
194
|
-
|
|
195
|
-
downcased = File.basename( target ).downcase
|
|
196
|
-
repos.find { |repo_path| File.basename( repo_path ).downcase == downcased }
|
|
197
|
-
end
|
|
198
|
-
|
|
199
121
|
# Unified output — JSON or human-readable.
|
|
200
122
|
def housekeep_finish( result:, exit_code:, json_output:, results: nil, succeeded: nil, failed: nil )
|
|
201
123
|
result[ :exit_code ] = exit_code
|
|
@@ -4,12 +4,12 @@ require "json"
|
|
|
4
4
|
|
|
5
5
|
module Carson
|
|
6
6
|
class Runtime
|
|
7
|
-
module
|
|
8
|
-
def
|
|
7
|
+
module List
|
|
8
|
+
def list!( json_output: false )
|
|
9
9
|
repos = config.govern_repos
|
|
10
10
|
|
|
11
11
|
if json_output
|
|
12
|
-
output.puts JSON.pretty_generate( { command: "
|
|
12
|
+
output.puts JSON.pretty_generate( { command: "list", repos: repos } )
|
|
13
13
|
else
|
|
14
14
|
if repos.empty?
|
|
15
15
|
puts_line "No governed repositories."
|
|
@@ -24,6 +24,6 @@ module Carson
|
|
|
24
24
|
end
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
-
include
|
|
27
|
+
include List
|
|
28
28
|
end
|
|
29
29
|
end
|
|
@@ -57,7 +57,7 @@ module Carson
|
|
|
57
57
|
|
|
58
58
|
# Canonical hook template location inside Carson repository.
|
|
59
59
|
def hook_template_path( hook_name: )
|
|
60
|
-
File.join( tool_root, "hooks", hook_name )
|
|
60
|
+
File.join( tool_root, "config", ".github", "hooks", hook_name )
|
|
61
61
|
end
|
|
62
62
|
|
|
63
63
|
# Reports full hook health and can enforce stricter action messaging in `check`.
|
|
@@ -144,56 +144,6 @@ module Carson
|
|
|
144
144
|
failed.zero? && pending.zero? ? EXIT_OK : EXIT_ERROR
|
|
145
145
|
end
|
|
146
146
|
|
|
147
|
-
def prune_all!
|
|
148
|
-
repos = config.govern_repos
|
|
149
|
-
if repos.empty?
|
|
150
|
-
puts_line "No governed repositories configured."
|
|
151
|
-
puts_line " Run carson onboard in each repo to register."
|
|
152
|
-
return EXIT_ERROR
|
|
153
|
-
end
|
|
154
|
-
|
|
155
|
-
puts_line ""
|
|
156
|
-
puts_line "Prune all (#{repos.length} repo#{plural_suffix( count: repos.length )})"
|
|
157
|
-
succeeded = 0
|
|
158
|
-
failed = 0
|
|
159
|
-
|
|
160
|
-
repos.each do |repo_path|
|
|
161
|
-
repo_name = File.basename( repo_path )
|
|
162
|
-
unless Dir.exist?( repo_path )
|
|
163
|
-
puts_line "#{repo_name}: not found"
|
|
164
|
-
record_batch_skip( command: "prune", repo_path: repo_path, reason: "path not found" )
|
|
165
|
-
failed += 1
|
|
166
|
-
next
|
|
167
|
-
end
|
|
168
|
-
|
|
169
|
-
begin
|
|
170
|
-
buffer = verbose? ? output : StringIO.new
|
|
171
|
-
error_buffer = verbose? ? error : StringIO.new
|
|
172
|
-
scoped_runtime = Runtime.new( repo_root: repo_path, tool_root: tool_root, output: buffer, error: error_buffer, verbose: verbose? )
|
|
173
|
-
status = scoped_runtime.prune!
|
|
174
|
-
unless verbose?
|
|
175
|
-
summary = buffer.string.lines.last.to_s.strip
|
|
176
|
-
puts_line "#{repo_name}: #{summary.empty? ? 'OK' : summary}"
|
|
177
|
-
end
|
|
178
|
-
if status == EXIT_ERROR
|
|
179
|
-
record_batch_skip( command: "prune", repo_path: repo_path, reason: "prune failed" )
|
|
180
|
-
failed += 1
|
|
181
|
-
else
|
|
182
|
-
clear_batch_success( command: "prune", repo_path: repo_path )
|
|
183
|
-
succeeded += 1
|
|
184
|
-
end
|
|
185
|
-
rescue StandardError => exception
|
|
186
|
-
puts_line "#{repo_name}: could not complete (#{exception.message})"
|
|
187
|
-
record_batch_skip( command: "prune", repo_path: repo_path, reason: exception.message )
|
|
188
|
-
failed += 1
|
|
189
|
-
end
|
|
190
|
-
end
|
|
191
|
-
|
|
192
|
-
puts_line ""
|
|
193
|
-
puts_line "Prune all complete: #{succeeded} pruned, #{failed} failed."
|
|
194
|
-
failed.zero? ? EXIT_OK : EXIT_ERROR
|
|
195
|
-
end
|
|
196
|
-
|
|
197
147
|
# Removes Carson-managed repository integration so a host repository can retire Carson cleanly.
|
|
198
148
|
def offboard!
|
|
199
149
|
puts_verbose ""
|
|
@@ -60,53 +60,6 @@ module Carson
|
|
|
60
60
|
git_system!( "switch", start_branch ) if switched && branch_exists?( branch_name: start_branch )
|
|
61
61
|
end
|
|
62
62
|
|
|
63
|
-
# Syncs main branch across all governed repositories.
|
|
64
|
-
def sync_all!
|
|
65
|
-
repos = config.govern_repos
|
|
66
|
-
if repos.empty?
|
|
67
|
-
puts_line "No governed repositories configured."
|
|
68
|
-
puts_line " Run carson onboard in each repo to register."
|
|
69
|
-
return EXIT_ERROR
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
puts_line ""
|
|
73
|
-
puts_line "Sync all (#{repos.length} repo#{plural_suffix( count: repos.length )})"
|
|
74
|
-
synced = 0
|
|
75
|
-
failed = 0
|
|
76
|
-
|
|
77
|
-
repos.each do |repo_path|
|
|
78
|
-
repo_name = File.basename( repo_path )
|
|
79
|
-
unless Dir.exist?( repo_path )
|
|
80
|
-
puts_line "#{repo_name}: not found"
|
|
81
|
-
record_batch_skip( command: "sync", repo_path: repo_path, reason: "path not found" )
|
|
82
|
-
failed += 1
|
|
83
|
-
next
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
begin
|
|
87
|
-
scoped_runtime = build_scoped_runtime( repo_path: repo_path )
|
|
88
|
-
status = scoped_runtime.sync!
|
|
89
|
-
if status == EXIT_OK
|
|
90
|
-
puts_line "#{repo_name}: ok" unless verbose?
|
|
91
|
-
clear_batch_success( command: "sync", repo_path: repo_path )
|
|
92
|
-
synced += 1
|
|
93
|
-
else
|
|
94
|
-
puts_line "#{repo_name}: could not sync" unless verbose?
|
|
95
|
-
record_batch_skip( command: "sync", repo_path: repo_path, reason: "sync failed" )
|
|
96
|
-
failed += 1
|
|
97
|
-
end
|
|
98
|
-
rescue StandardError => exception
|
|
99
|
-
puts_line "#{repo_name}: could not sync (#{exception.message})"
|
|
100
|
-
record_batch_skip( command: "sync", repo_path: repo_path, reason: exception.message )
|
|
101
|
-
failed += 1
|
|
102
|
-
end
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
puts_line ""
|
|
106
|
-
puts_line "Sync all complete: #{synced} synced, #{failed} failed."
|
|
107
|
-
failed.zero? ? EXIT_OK : EXIT_ERROR
|
|
108
|
-
end
|
|
109
|
-
|
|
110
63
|
private
|
|
111
64
|
|
|
112
65
|
# Runs a git command, suppressing stdout/stderr in JSON mode to keep output clean.
|
|
@@ -181,7 +134,7 @@ module Carson
|
|
|
181
134
|
end
|
|
182
135
|
|
|
183
136
|
def main_worktree_context?
|
|
184
|
-
realpath_safe(
|
|
137
|
+
realpath_safe( work_dir ) == realpath_safe( main_worktree_root )
|
|
185
138
|
end
|
|
186
139
|
|
|
187
140
|
def inside_git_work_tree?
|
|
@@ -53,53 +53,6 @@ module Carson
|
|
|
53
53
|
( drift_count + stale_count ).positive? ? EXIT_BLOCK : EXIT_OK
|
|
54
54
|
end
|
|
55
55
|
|
|
56
|
-
# Read-only template drift check across all governed repositories.
|
|
57
|
-
def template_check_all!
|
|
58
|
-
repos = config.govern_repos
|
|
59
|
-
if repos.empty?
|
|
60
|
-
puts_line "No governed repositories configured."
|
|
61
|
-
puts_line " Run carson onboard in each repo to register."
|
|
62
|
-
return EXIT_ERROR
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
puts_line ""
|
|
66
|
-
puts_line "Template check all (#{repos.length} repo#{plural_suffix( count: repos.length )})"
|
|
67
|
-
in_sync = 0
|
|
68
|
-
drifted = 0
|
|
69
|
-
failed = 0
|
|
70
|
-
|
|
71
|
-
repos.each do |repo_path|
|
|
72
|
-
repo_name = File.basename( repo_path )
|
|
73
|
-
unless Dir.exist?( repo_path )
|
|
74
|
-
puts_line "#{repo_name}: not found"
|
|
75
|
-
record_batch_skip( command: "template_check", repo_path: repo_path, reason: "path not found" )
|
|
76
|
-
failed += 1
|
|
77
|
-
next
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
begin
|
|
81
|
-
scoped_runtime = build_scoped_runtime( repo_path: repo_path )
|
|
82
|
-
status = scoped_runtime.template_check!
|
|
83
|
-
if status == EXIT_OK
|
|
84
|
-
puts_line "#{repo_name}: in sync" unless verbose?
|
|
85
|
-
clear_batch_success( command: "template_check", repo_path: repo_path )
|
|
86
|
-
in_sync += 1
|
|
87
|
-
else
|
|
88
|
-
puts_line "#{repo_name}: DRIFT" unless verbose?
|
|
89
|
-
drifted += 1
|
|
90
|
-
end
|
|
91
|
-
rescue StandardError => exception
|
|
92
|
-
puts_line "#{repo_name}: could not complete (#{exception.message})"
|
|
93
|
-
record_batch_skip( command: "template_check", repo_path: repo_path, reason: exception.message )
|
|
94
|
-
failed += 1
|
|
95
|
-
end
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
puts_line ""
|
|
99
|
-
puts_line "Template check complete: #{in_sync} in sync, #{drifted} drifted, #{failed} failed."
|
|
100
|
-
drifted.zero? && failed.zero? ? EXIT_OK : EXIT_BLOCK
|
|
101
|
-
end
|
|
102
|
-
|
|
103
56
|
# Applies managed template files as full-file writes from Carson sources.
|
|
104
57
|
# Also removes superseded files that are no longer part of the managed set.
|
|
105
58
|
def template_apply!( push_prep: false )
|
|
@@ -1,46 +1,47 @@
|
|
|
1
|
-
# Carson
|
|
2
|
-
#
|
|
1
|
+
# Carson receive — single-repo delivery triage.
|
|
2
|
+
# Receive reassesses queued/gated deliveries, records revision cycles, and integrates one ready delivery at a time.
|
|
3
3
|
require "json"
|
|
4
4
|
require "time"
|
|
5
5
|
|
|
6
6
|
module Carson
|
|
7
7
|
class Runtime
|
|
8
|
-
module
|
|
9
|
-
#
|
|
10
|
-
def
|
|
8
|
+
module Receive
|
|
9
|
+
# Single-repo entry point. Advances deliveries for the current repository.
|
|
10
|
+
def receive!( dry_run: false, json_output: false, loop_seconds: nil )
|
|
11
11
|
if loop_seconds
|
|
12
|
-
|
|
12
|
+
receive_loop!( dry_run: dry_run, json_output: json_output, loop_seconds: loop_seconds )
|
|
13
13
|
else
|
|
14
|
-
|
|
14
|
+
receive_cycle!( dry_run: dry_run, json_output: json_output )
|
|
15
15
|
end
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
-
def
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
print_header "
|
|
18
|
+
def receive_cycle!( dry_run:, json_output: )
|
|
19
|
+
repo_path = repository_record.path
|
|
20
|
+
repo_name = File.basename( repo_path )
|
|
21
|
+
print_header "Receiving #{repo_name}" unless json_output
|
|
22
22
|
|
|
23
|
+
repo_report = receive_repo!( repo_path: repo_path, dry_run: dry_run, silent: json_output )
|
|
23
24
|
report = {
|
|
24
25
|
cycle_at: Time.now.utc.iso8601,
|
|
25
26
|
dry_run: dry_run,
|
|
26
|
-
|
|
27
|
+
repository: repo_report
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
if json_output
|
|
30
31
|
output.puts JSON.pretty_generate( report )
|
|
31
32
|
else
|
|
32
|
-
|
|
33
|
+
print_receive_summary( repo_report: repo_report )
|
|
33
34
|
end
|
|
34
35
|
|
|
35
36
|
EXIT_OK
|
|
36
37
|
rescue StandardError => exception
|
|
37
|
-
puts_line "
|
|
38
|
+
puts_line "Receive did not complete: #{exception.message}"
|
|
38
39
|
EXIT_ERROR
|
|
39
40
|
end
|
|
40
41
|
|
|
41
|
-
def
|
|
42
|
+
def receive_loop!( dry_run:, json_output:, loop_seconds: )
|
|
42
43
|
run_signal_aware_loop!(
|
|
43
|
-
loop_name: "
|
|
44
|
+
loop_name: "receive",
|
|
44
45
|
loop_seconds: loop_seconds,
|
|
45
46
|
cycle_line: ->( cycle_count ) { "cycle #{cycle_count} at #{Time.now.utc.strftime( '%Y-%m-%d %H:%M:%S UTC' )}" },
|
|
46
47
|
sleep_line: ->( seconds ) do
|
|
@@ -48,21 +49,13 @@ module Carson
|
|
|
48
49
|
"sleeping #{seconds}s — next cycle at #{next_at.strftime( '%Y-%m-%d %H:%M:%S %z' )}"
|
|
49
50
|
end
|
|
50
51
|
) do
|
|
51
|
-
|
|
52
|
+
receive_cycle!( dry_run: dry_run, json_output: json_output )
|
|
52
53
|
end
|
|
53
54
|
end
|
|
54
55
|
|
|
55
56
|
private
|
|
56
57
|
|
|
57
|
-
def
|
|
58
|
-
config.govern_repos.map do |path|
|
|
59
|
-
expanded = File.expand_path( path )
|
|
60
|
-
next nil unless Dir.exist?( expanded )
|
|
61
|
-
expanded
|
|
62
|
-
end.compact
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
def govern_repo!( repo_path:, dry_run:, silent: false )
|
|
58
|
+
def receive_repo!( repo_path:, dry_run:, silent: false )
|
|
66
59
|
scoped_runtime = repo_runtime_for( repo_path: repo_path )
|
|
67
60
|
repository = Repository.new( path: repo_path, runtime: scoped_runtime )
|
|
68
61
|
deliveries = scoped_runtime.ledger.active_deliveries( repo_path: repo_path )
|
|
@@ -365,10 +358,10 @@ module Carson
|
|
|
365
358
|
def delivery_action_hint( delivery:, next_to_integrate:, dry_run: )
|
|
366
359
|
return nil if dry_run
|
|
367
360
|
return nil if delivery.superseded? || delivery.integrated? || delivery.failed?
|
|
368
|
-
return "integrating
|
|
361
|
+
return "integrating..." if delivery.ready? && delivery.key == next_to_integrate
|
|
369
362
|
return nil unless delivery.blocked?
|
|
370
363
|
return nil if held_delivery?( delivery: delivery )
|
|
371
|
-
delivery.revision_count >= 3 ? "escalating
|
|
364
|
+
delivery.revision_count >= 3 ? "escalating..." : "revising..."
|
|
372
365
|
end
|
|
373
366
|
|
|
374
367
|
def housekeep_repo!( repo_path: )
|
|
@@ -514,28 +507,26 @@ module Carson
|
|
|
514
507
|
""
|
|
515
508
|
end
|
|
516
509
|
|
|
517
|
-
def
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
end
|
|
510
|
+
def print_receive_summary( repo_report: )
|
|
511
|
+
if repo_report[ :error ]
|
|
512
|
+
puts_line "#{repo_report[ :repository ]}: #{repo_report[ :error ]}"
|
|
513
|
+
return
|
|
514
|
+
end
|
|
523
515
|
|
|
524
|
-
|
|
516
|
+
return if repo_report[ :deliveries ].empty?
|
|
525
517
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
end
|
|
532
|
-
end
|
|
518
|
+
repo_report[ :deliveries ].each do |delivery|
|
|
519
|
+
action_text = format_receive_action( status: delivery[ :status ], action: delivery[ :action ], cause: delivery[ :cause ] )
|
|
520
|
+
puts_line "#{repo_report[ :repository ]}/#{delivery[ :branch ]} — #{action_text}"
|
|
521
|
+
puts_line " #{delivery[ :summary ]}" unless delivery[ :summary ].to_s.empty?
|
|
522
|
+
puts_line " Merge proof: #{delivery.dig( :merge_proof, :summary )}" if delivery[ :merge_proof ]
|
|
533
523
|
end
|
|
524
|
+
end
|
|
534
525
|
|
|
535
|
-
def
|
|
526
|
+
def format_receive_action( status:, action:, cause: )
|
|
536
527
|
case action
|
|
537
528
|
when "integrate"
|
|
538
|
-
|
|
529
|
+
format_receive_integration_outcome( status: status, cause: cause )
|
|
539
530
|
when "would_integrate" then "ready to integrate (dry run)"
|
|
540
531
|
when "hold" then cause == "freshness" ? "refresh required" : "held at gate"
|
|
541
532
|
when "would_hold" then cause == "freshness" ? "would require refresh (dry run)" : "would hold at gate (dry run)"
|
|
@@ -547,7 +538,7 @@ module Carson
|
|
|
547
538
|
end
|
|
548
539
|
end
|
|
549
540
|
|
|
550
|
-
def
|
|
541
|
+
def format_receive_integration_outcome( status:, cause: )
|
|
551
542
|
case status
|
|
552
543
|
when "integrated" then "integrated"
|
|
553
544
|
when "gated" then cause == "freshness" ? "refresh required" : "held at gate"
|
|
@@ -558,6 +549,6 @@ module Carson
|
|
|
558
549
|
end
|
|
559
550
|
end
|
|
560
551
|
|
|
561
|
-
include
|
|
552
|
+
include Receive
|
|
562
553
|
end
|
|
563
554
|
end
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
module Carson
|
|
3
3
|
class Runtime
|
|
4
4
|
module Recover
|
|
5
|
-
GOVERNANCE_SURFACE_PREFIXES = %w[ .github/
|
|
5
|
+
GOVERNANCE_SURFACE_PREFIXES = %w[ .github/ config/.github/ ].freeze
|
|
6
6
|
|
|
7
7
|
def recover!( check_name:, json_output: false )
|
|
8
8
|
result = {
|
|
@@ -71,7 +71,7 @@ module Carson
|
|
|
71
71
|
|
|
72
72
|
unless relation.fetch( :related )
|
|
73
73
|
result[ :error ] = "branch does not touch the governance surface for #{check_name}"
|
|
74
|
-
result[ :recovery ] = "update the branch to repair .github/ or
|
|
74
|
+
result[ :recovery ] = "update the branch to repair .github/ or config/.github/, then rerun carson recover --check #{check_name.inspect}"
|
|
75
75
|
return recover_finish( result: result, exit_code: EXIT_BLOCK, json_output: json_output )
|
|
76
76
|
end
|
|
77
77
|
|
|
@@ -15,40 +15,6 @@ module Carson
|
|
|
15
15
|
EXIT_OK
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
-
# Portfolio-wide status overview across all governed repositories.
|
|
19
|
-
def status_all!( json_output: false )
|
|
20
|
-
repositories = config.govern_repos
|
|
21
|
-
if repositories.empty?
|
|
22
|
-
puts_line "No governed repositories configured."
|
|
23
|
-
puts_line " Run carson onboard in each repo to register."
|
|
24
|
-
return EXIT_ERROR
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
results = repositories.map do |repo_path|
|
|
28
|
-
repo_name = File.basename( repo_path )
|
|
29
|
-
unless Dir.exist?( repo_path )
|
|
30
|
-
{ name: repo_name, status: "error", error: "not found" }
|
|
31
|
-
else
|
|
32
|
-
begin
|
|
33
|
-
scoped_runtime = build_scoped_runtime( repo_path: repo_path )
|
|
34
|
-
{ name: repo_name, status: "ok" }.merge( scoped_runtime.send( :gather_status ) )
|
|
35
|
-
rescue StandardError => exception
|
|
36
|
-
{ name: repo_name, status: "error", error: exception.message }
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
if json_output
|
|
42
|
-
output.puts JSON.pretty_generate( { command: "status", repos: results, repositories: results } )
|
|
43
|
-
else
|
|
44
|
-
puts_line "Carson #{Carson::VERSION} — Portfolio (#{repositories.length} repo#{plural_suffix( count: repositories.length )})"
|
|
45
|
-
puts_line ""
|
|
46
|
-
results.each { |result| print_portfolio_status( result: result ) }
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
EXIT_OK
|
|
50
|
-
end
|
|
51
|
-
|
|
52
18
|
private
|
|
53
19
|
|
|
54
20
|
def gather_status
|
|
@@ -108,7 +74,7 @@ module Carson
|
|
|
108
74
|
end
|
|
109
75
|
|
|
110
76
|
def main_worktree_context?
|
|
111
|
-
realpath_safe(
|
|
77
|
+
realpath_safe( work_dir ) == realpath_safe( main_worktree_root )
|
|
112
78
|
end
|
|
113
79
|
|
|
114
80
|
def remote_sync_status( branch: )
|
|
@@ -181,18 +147,6 @@ module Carson
|
|
|
181
147
|
end
|
|
182
148
|
end
|
|
183
149
|
|
|
184
|
-
def print_portfolio_status( result: )
|
|
185
|
-
if result.fetch( :status ) == "error"
|
|
186
|
-
puts_line "#{result.fetch( :name )}: #{result.fetch( :error )}"
|
|
187
|
-
return
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
deliveries = Array( result.fetch( :branches, [] ) )
|
|
191
|
-
counts = deliveries.each_with_object( Hash.new( 0 ) ) { |delivery, memo| memo[ delivery.fetch( :delivery_state ) ] += 1 }
|
|
192
|
-
summary = counts.empty? ? "no active deliveries" : counts.map { |state, count| "#{count} #{state}" }.join( ", " )
|
|
193
|
-
puts_line "#{result.fetch( :name )} — #{summary}"
|
|
194
|
-
end
|
|
195
|
-
|
|
196
150
|
def format_sync( sync: )
|
|
197
151
|
case sync
|
|
198
152
|
when :in_sync then "in sync with remote"
|
data/lib/carson/runtime.rb
CHANGED
|
@@ -23,16 +23,20 @@ module Carson
|
|
|
23
23
|
DISPOSITION_TOKENS = %w[accepted rejected deferred].freeze
|
|
24
24
|
|
|
25
25
|
# Runtime wiring for repository context, tool paths, and output streams.
|
|
26
|
-
|
|
26
|
+
# work_dir: the actual directory for git/gh command execution. When running
|
|
27
|
+
# from a worktree, this is the worktree path; repo_root remains the canonical
|
|
28
|
+
# main tree root for config, ledger, and path resolution.
|
|
29
|
+
def initialize( repo_root:, tool_root:, output:, error:, in_stream: $stdin, verbose: false, work_dir: nil )
|
|
27
30
|
@repo_root = repo_root
|
|
31
|
+
@work_dir = work_dir || repo_root
|
|
28
32
|
@tool_root = tool_root
|
|
29
33
|
@output = output
|
|
30
34
|
@error = error
|
|
31
35
|
@in = in_stream
|
|
32
36
|
@verbose = verbose
|
|
33
37
|
@config = Config.load( repo_root: repo_root )
|
|
34
|
-
@git_adapter = Adapters::Git.new( repo_root:
|
|
35
|
-
@github_adapter = Adapters::GitHub.new( repo_root:
|
|
38
|
+
@git_adapter = Adapters::Git.new( repo_root: @work_dir )
|
|
39
|
+
@github_adapter = Adapters::GitHub.new( repo_root: @work_dir )
|
|
36
40
|
@template_sync_result = nil
|
|
37
41
|
end
|
|
38
42
|
|
|
@@ -47,7 +51,7 @@ module Carson
|
|
|
47
51
|
|
|
48
52
|
private
|
|
49
53
|
|
|
50
|
-
attr_reader :repo_root, :tool_root, :output, :error, :in, :config, :git_adapter, :github_adapter
|
|
54
|
+
attr_reader :repo_root, :work_dir, :tool_root, :output, :error, :in, :config, :git_adapter, :github_adapter
|
|
51
55
|
|
|
52
56
|
# Ruby 2.6 treats bare `in` awkwardly because of pattern-matching parsing.
|
|
53
57
|
# Keep the original ivar/reader for compatibility, but expose a safe helper name.
|
|
@@ -95,7 +99,7 @@ module Carson
|
|
|
95
99
|
# Passive repository record for the current runtime context.
|
|
96
100
|
# Uses main_worktree_root so the repo_path stored in the ledger is always the
|
|
97
101
|
# canonical main tree path, regardless of which worktree the command runs from.
|
|
98
|
-
# This ensures
|
|
102
|
+
# This ensures receive (which looks up by main tree path) finds worktree deliveries.
|
|
99
103
|
def repository_record
|
|
100
104
|
Repository.new( path: main_worktree_root, runtime: self )
|
|
101
105
|
end
|
|
@@ -372,9 +376,9 @@ require_relative "runtime/local"
|
|
|
372
376
|
require_relative "runtime/audit"
|
|
373
377
|
require_relative "runtime/loop_runner"
|
|
374
378
|
require_relative "runtime/housekeep"
|
|
375
|
-
require_relative "runtime/
|
|
379
|
+
require_relative "runtime/list"
|
|
376
380
|
require_relative "runtime/review"
|
|
377
|
-
require_relative "runtime/
|
|
381
|
+
require_relative "runtime/receive"
|
|
378
382
|
require_relative "runtime/setup"
|
|
379
383
|
require_relative "runtime/status"
|
|
380
384
|
require_relative "runtime/abandon"
|