carson 3.21.1 → 3.22.1
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 +4 -9
- data/MANUAL.md +45 -28
- data/README.md +45 -50
- data/RELEASE.md +24 -1
- data/SKILL.md +1 -1
- data/VERSION +1 -1
- data/carson.gemspec +2 -4
- data/hooks/command-guard +1 -1
- data/hooks/pre-push +17 -20
- data/lib/carson/cli.rb +55 -16
- data/lib/carson/config.rb +55 -14
- data/lib/carson/runtime/audit.rb +42 -17
- data/lib/carson/runtime/deliver.rb +118 -51
- data/lib/carson/runtime/govern.rb +29 -17
- data/lib/carson/runtime/housekeep.rb +231 -25
- data/lib/carson/runtime/local/onboard.rb +24 -24
- data/lib/carson/runtime/local/prune.rb +131 -35
- data/lib/carson/runtime/local/sync.rb +29 -7
- data/lib/carson/runtime/local/template.rb +26 -8
- data/lib/carson/runtime/local/worktree.rb +37 -442
- data/lib/carson/runtime/review/gate_support.rb +131 -1
- data/lib/carson/runtime/review.rb +21 -77
- data/lib/carson/runtime/setup.rb +15 -6
- data/lib/carson/runtime/status.rb +36 -13
- data/lib/carson/runtime.rb +18 -4
- data/lib/carson/worktree.rb +497 -0
- data/lib/carson.rb +1 -0
- metadata +11 -16
- data/.github/copilot-instructions.md +0 -1
- data/.github/pull_request_template.md +0 -12
- data/templates/.github/AGENTS.md +0 -1
- data/templates/.github/CLAUDE.md +0 -1
- data/templates/.github/carson.md +0 -47
- data/templates/.github/copilot-instructions.md +0 -1
- data/templates/.github/pull_request_template.md +0 -12
data/lib/carson/cli.rb
CHANGED
|
@@ -4,6 +4,8 @@ require "optparse"
|
|
|
4
4
|
module Carson
|
|
5
5
|
class CLI
|
|
6
6
|
def self.start( arguments:, repo_root:, tool_root:, output:, error: )
|
|
7
|
+
ensure_global_artefacts!( tool_root: tool_root )
|
|
8
|
+
|
|
7
9
|
parsed = parse_args( arguments: arguments, output: output, error: error )
|
|
8
10
|
command = parsed.fetch( :command )
|
|
9
11
|
return Runtime::EXIT_OK if command == :help
|
|
@@ -22,7 +24,7 @@ module Carson
|
|
|
22
24
|
target_repo_root = parsed.fetch( :repo_root, nil )
|
|
23
25
|
target_repo_root = repo_root if target_repo_root.to_s.strip.empty?
|
|
24
26
|
unless Dir.exist?( target_repo_root )
|
|
25
|
-
error.puts "#{BADGE}
|
|
27
|
+
error.puts "#{BADGE} Repository path not found: #{target_repo_root}"
|
|
26
28
|
return Runtime::EXIT_ERROR
|
|
27
29
|
end
|
|
28
30
|
|
|
@@ -30,10 +32,10 @@ module Carson
|
|
|
30
32
|
runtime = Runtime.new( repo_root: target_repo_root, tool_root: tool_root, output: output, error: error, verbose: verbose )
|
|
31
33
|
dispatch( parsed: parsed, runtime: runtime )
|
|
32
34
|
rescue ConfigError => exception
|
|
33
|
-
error.puts "#{BADGE}
|
|
35
|
+
error.puts "#{BADGE} Configuration problem: #{exception.message}"
|
|
34
36
|
Runtime::EXIT_ERROR
|
|
35
37
|
rescue StandardError => exception
|
|
36
|
-
error.puts "#{BADGE}
|
|
38
|
+
error.puts "#{BADGE} #{exception.message}"
|
|
37
39
|
Runtime::EXIT_ERROR
|
|
38
40
|
end
|
|
39
41
|
|
|
@@ -147,7 +149,7 @@ module Carson
|
|
|
147
149
|
parser.on( "--main-branch NAME", "Main branch name" ) { |value| options[ "git.main_branch" ] = value }
|
|
148
150
|
parser.on( "--workflow STYLE", "Workflow style (branch or trunk)" ) { |value| options[ "workflow.style" ] = value }
|
|
149
151
|
parser.on( "--merge METHOD", "Merge method (squash, rebase, or merge)" ) { |value| options[ "govern.merge.method" ] = value }
|
|
150
|
-
parser.on( "--canonical PATH", "Canonical
|
|
152
|
+
parser.on( "--canonical PATH", "Canonical lint policy directory path" ) { |value| options[ "lint.canonical" ] = value }
|
|
151
153
|
parser.separator ""
|
|
152
154
|
parser.separator "Examples:"
|
|
153
155
|
parser.separator " carson setup Auto-detect and write config"
|
|
@@ -163,6 +165,7 @@ module Carson
|
|
|
163
165
|
{ command: "setup", cli_choices: options }
|
|
164
166
|
rescue OptionParser::ParseError => exception
|
|
165
167
|
error.puts "#{BADGE} #{exception.message}"
|
|
168
|
+
error.puts setup_parser
|
|
166
169
|
{ command: :invalid }
|
|
167
170
|
end
|
|
168
171
|
|
|
@@ -193,6 +196,7 @@ module Carson
|
|
|
193
196
|
}
|
|
194
197
|
rescue OptionParser::ParseError => exception
|
|
195
198
|
error.puts "#{BADGE} #{exception.message}"
|
|
199
|
+
error.puts onboard_parser
|
|
196
200
|
{ command: :invalid }
|
|
197
201
|
end
|
|
198
202
|
|
|
@@ -220,6 +224,7 @@ module Carson
|
|
|
220
224
|
}
|
|
221
225
|
rescue OptionParser::ParseError => exception
|
|
222
226
|
error.puts "#{BADGE} #{exception.message}"
|
|
227
|
+
error.puts offboard_parser
|
|
223
228
|
{ command: :invalid }
|
|
224
229
|
end
|
|
225
230
|
|
|
@@ -263,6 +268,7 @@ module Carson
|
|
|
263
268
|
}
|
|
264
269
|
rescue OptionParser::ParseError => exception
|
|
265
270
|
error.puts "#{BADGE} #{exception.message}"
|
|
271
|
+
error.puts refresh_parser
|
|
266
272
|
{ command: :invalid }
|
|
267
273
|
end
|
|
268
274
|
|
|
@@ -290,6 +296,7 @@ module Carson
|
|
|
290
296
|
{ command: "prune", json: options[ :json ] }
|
|
291
297
|
rescue OptionParser::ParseError => exception
|
|
292
298
|
error.puts "#{BADGE} #{exception.message}"
|
|
299
|
+
error.puts prune_parser
|
|
293
300
|
{ command: :invalid }
|
|
294
301
|
end
|
|
295
302
|
|
|
@@ -346,6 +353,7 @@ module Carson
|
|
|
346
353
|
end
|
|
347
354
|
rescue OptionParser::ParseError => exception
|
|
348
355
|
error.puts "#{BADGE} #{exception.message}"
|
|
356
|
+
error.puts worktree_parser
|
|
349
357
|
{ command: :invalid }
|
|
350
358
|
end
|
|
351
359
|
|
|
@@ -376,6 +384,7 @@ module Carson
|
|
|
376
384
|
{ command: "review:#{action}" }
|
|
377
385
|
rescue OptionParser::ParseError => exception
|
|
378
386
|
error.puts "#{BADGE} #{exception.message}"
|
|
387
|
+
error.puts review_parser
|
|
379
388
|
{ command: :invalid }
|
|
380
389
|
end
|
|
381
390
|
|
|
@@ -434,6 +443,7 @@ module Carson
|
|
|
434
443
|
{ command: "template:apply", push_prep: options.fetch( :push_prep ) }
|
|
435
444
|
rescue OptionParser::ParseError => exception
|
|
436
445
|
error.puts "#{BADGE} #{exception.message}"
|
|
446
|
+
error.puts( apply_parser || template_parser )
|
|
437
447
|
{ command: :invalid }
|
|
438
448
|
end
|
|
439
449
|
|
|
@@ -466,6 +476,7 @@ module Carson
|
|
|
466
476
|
{ command: "audit", json: options[ :json ] }
|
|
467
477
|
rescue OptionParser::ParseError => exception
|
|
468
478
|
error.puts "#{BADGE} #{exception.message}"
|
|
479
|
+
error.puts audit_parser
|
|
469
480
|
{ command: :invalid }
|
|
470
481
|
end
|
|
471
482
|
|
|
@@ -497,6 +508,7 @@ module Carson
|
|
|
497
508
|
{ command: "sync", json: options[ :json ] }
|
|
498
509
|
rescue OptionParser::ParseError => exception
|
|
499
510
|
error.puts "#{BADGE} #{exception.message}"
|
|
511
|
+
error.puts sync_parser
|
|
500
512
|
{ command: :invalid }
|
|
501
513
|
end
|
|
502
514
|
|
|
@@ -528,6 +540,7 @@ module Carson
|
|
|
528
540
|
{ command: "status", json: options[ :json ] }
|
|
529
541
|
rescue OptionParser::ParseError => exception
|
|
530
542
|
error.puts "#{BADGE} #{exception.message}"
|
|
543
|
+
error.puts status_parser
|
|
531
544
|
{ command: :invalid }
|
|
532
545
|
end
|
|
533
546
|
|
|
@@ -566,6 +579,7 @@ module Carson
|
|
|
566
579
|
}
|
|
567
580
|
rescue OptionParser::ParseError => exception
|
|
568
581
|
error.puts "#{BADGE} #{exception.message}"
|
|
582
|
+
error.puts deliver_parser
|
|
569
583
|
{ command: :invalid }
|
|
570
584
|
end
|
|
571
585
|
|
|
@@ -594,27 +608,30 @@ module Carson
|
|
|
594
608
|
{ command: "repos", json: options[ :json ] }
|
|
595
609
|
rescue OptionParser::ParseError => exception
|
|
596
610
|
error.puts "#{BADGE} #{exception.message}"
|
|
611
|
+
error.puts repos_parser
|
|
597
612
|
{ command: :invalid }
|
|
598
613
|
end
|
|
599
614
|
|
|
600
615
|
# --- housekeep ---
|
|
601
616
|
|
|
602
617
|
def self.parse_housekeep_command( arguments:, error: )
|
|
603
|
-
options = { all: false, json: false }
|
|
618
|
+
options = { all: false, json: false, dry_run: false }
|
|
604
619
|
housekeep_parser = OptionParser.new do |parser|
|
|
605
|
-
parser.banner = "Usage: carson housekeep [REPO] [--all] [--json]"
|
|
620
|
+
parser.banner = "Usage: carson housekeep [REPO] [--all] [--dry-run] [--json]"
|
|
606
621
|
parser.separator ""
|
|
607
622
|
parser.separator "Run housekeeping: sync main, reap dead worktrees, and prune stale branches."
|
|
608
623
|
parser.separator "Defaults to the current repository."
|
|
609
624
|
parser.separator ""
|
|
610
625
|
parser.separator "Options:"
|
|
611
626
|
parser.on( "--all", "Housekeep all governed repositories" ) { options[ :all ] = true }
|
|
627
|
+
parser.on( "--dry-run", "Show what would be reaped/deleted without making changes" ) { options[ :dry_run ] = true }
|
|
612
628
|
parser.on( "--json", "Machine-readable JSON output" ) { options[ :json ] = true }
|
|
613
629
|
parser.separator ""
|
|
614
630
|
parser.separator "Examples:"
|
|
615
|
-
parser.separator " carson housekeep
|
|
616
|
-
parser.separator " carson housekeep
|
|
617
|
-
parser.separator " carson housekeep
|
|
631
|
+
parser.separator " carson housekeep Housekeep the current repository"
|
|
632
|
+
parser.separator " carson housekeep --dry-run Preview what housekeep would do"
|
|
633
|
+
parser.separator " carson housekeep nexus Housekeep a named governed repo"
|
|
634
|
+
parser.separator " carson housekeep --all Housekeep all governed repos"
|
|
618
635
|
end
|
|
619
636
|
housekeep_parser.parse!( arguments )
|
|
620
637
|
|
|
@@ -623,7 +640,7 @@ module Carson
|
|
|
623
640
|
return { command: :invalid }
|
|
624
641
|
end
|
|
625
642
|
|
|
626
|
-
return { command: "housekeep:all", json: options[ :json ] } if options[ :all ]
|
|
643
|
+
return { command: "housekeep:all", json: options[ :json ], dry_run: options[ :dry_run ] } if options[ :all ]
|
|
627
644
|
|
|
628
645
|
if arguments.length > 1
|
|
629
646
|
error.puts "#{BADGE} Too many arguments for housekeep. Use: carson housekeep [repo]"
|
|
@@ -631,11 +648,12 @@ module Carson
|
|
|
631
648
|
end
|
|
632
649
|
|
|
633
650
|
target = arguments.shift
|
|
634
|
-
return { command: "housekeep:target", target: target, json: options[ :json ] } if target
|
|
651
|
+
return { command: "housekeep:target", target: target, json: options[ :json ], dry_run: options[ :dry_run ] } if target
|
|
635
652
|
|
|
636
|
-
{ command: "housekeep", json: options[ :json ] }
|
|
653
|
+
{ command: "housekeep", json: options[ :json ], dry_run: options[ :dry_run ] }
|
|
637
654
|
rescue OptionParser::ParseError => exception
|
|
638
655
|
error.puts "#{BADGE} #{exception.message}"
|
|
656
|
+
error.puts housekeep_parser
|
|
639
657
|
{ command: :invalid }
|
|
640
658
|
end
|
|
641
659
|
|
|
@@ -658,7 +676,7 @@ module Carson
|
|
|
658
676
|
parser.on( "--dry-run", "Run all checks but do not merge or dispatch" ) { options[ :dry_run ] = true }
|
|
659
677
|
parser.on( "--json", "Machine-readable JSON output" ) { options[ :json ] = true }
|
|
660
678
|
parser.on( "--loop SECONDS", Integer, "Run continuously, sleeping SECONDS between cycles" ) do |seconds|
|
|
661
|
-
error.puts( "#{BADGE}
|
|
679
|
+
error.puts( "#{BADGE} --loop expects a positive integer" ) || ( return { command: :invalid } ) if seconds < 1
|
|
662
680
|
options[ :loop_seconds ] = seconds
|
|
663
681
|
end
|
|
664
682
|
parser.separator ""
|
|
@@ -685,6 +703,27 @@ module Carson
|
|
|
685
703
|
{ command: :invalid }
|
|
686
704
|
end
|
|
687
705
|
|
|
706
|
+
# --- global artefacts ---
|
|
707
|
+
|
|
708
|
+
# Ensures global (non-repo) artefacts are installed at CLI startup.
|
|
709
|
+
# The command-guard lives at a stable path (~/.carson/hooks/command-guard)
|
|
710
|
+
# referenced by Claude Code's PreToolUse hook. It must exist regardless of
|
|
711
|
+
# whether `carson refresh` has been run in any governed repo.
|
|
712
|
+
def self.ensure_global_artefacts!( tool_root: )
|
|
713
|
+
source = File.join( tool_root, "hooks", "command-guard" )
|
|
714
|
+
return unless File.file?( source )
|
|
715
|
+
|
|
716
|
+
hooks_base = File.expand_path( "~/.carson/hooks" )
|
|
717
|
+
target = File.join( hooks_base, "command-guard" )
|
|
718
|
+
return if File.file?( target ) && FileUtils.identical?( source, target )
|
|
719
|
+
|
|
720
|
+
FileUtils.mkdir_p( hooks_base )
|
|
721
|
+
FileUtils.cp( source, target )
|
|
722
|
+
FileUtils.chmod( 0o755, target )
|
|
723
|
+
rescue StandardError
|
|
724
|
+
# Best-effort — do not block any command if this fails.
|
|
725
|
+
end
|
|
726
|
+
|
|
688
727
|
# --- dispatch ---
|
|
689
728
|
|
|
690
729
|
def self.dispatch( parsed:, runtime: )
|
|
@@ -734,11 +773,11 @@ module Carson
|
|
|
734
773
|
when "repos"
|
|
735
774
|
runtime.repos!( json_output: parsed.fetch( :json, false ) )
|
|
736
775
|
when "housekeep"
|
|
737
|
-
runtime.housekeep!( json_output: parsed.fetch( :json, false ) )
|
|
776
|
+
runtime.housekeep!( json_output: parsed.fetch( :json, false ), dry_run: parsed.fetch( :dry_run, false ) )
|
|
738
777
|
when "housekeep:target"
|
|
739
|
-
runtime.housekeep_target!( target: parsed.fetch( :target ), json_output: parsed.fetch( :json, false ) )
|
|
778
|
+
runtime.housekeep_target!( target: parsed.fetch( :target ), json_output: parsed.fetch( :json, false ), dry_run: parsed.fetch( :dry_run, false ) )
|
|
740
779
|
when "housekeep:all"
|
|
741
|
-
runtime.housekeep_all!( json_output: parsed.fetch( :json, false ) )
|
|
780
|
+
runtime.housekeep_all!( json_output: parsed.fetch( :json, false ), dry_run: parsed.fetch( :dry_run, false ) )
|
|
742
781
|
when "govern"
|
|
743
782
|
runtime.govern!(
|
|
744
783
|
dry_run: parsed.fetch( :dry_run, false ),
|
data/lib/carson/config.rb
CHANGED
|
@@ -7,9 +7,24 @@ module Carson
|
|
|
7
7
|
|
|
8
8
|
# Config is built-in only for outsider mode; host repositories do not carry Carson config files.
|
|
9
9
|
class Config
|
|
10
|
+
CANONICAL_GITHUB_DIRECTORIES = %w[actions workflows ISSUE_TEMPLATE DISCUSSION_TEMPLATE linters].freeze
|
|
11
|
+
CANONICAL_GITHUB_ROOT_FILES = [
|
|
12
|
+
".mega-linter.yml",
|
|
13
|
+
"AGENTS.md",
|
|
14
|
+
"CLAUDE.md",
|
|
15
|
+
"CODEOWNERS",
|
|
16
|
+
"FUNDING.yml",
|
|
17
|
+
"carson.md",
|
|
18
|
+
"copilot-instructions.md",
|
|
19
|
+
"dependabot.yml",
|
|
20
|
+
"funding.yml",
|
|
21
|
+
"labeler.yml",
|
|
22
|
+
"pull_request_template.md"
|
|
23
|
+
].freeze
|
|
24
|
+
|
|
10
25
|
attr_accessor :git_remote
|
|
11
26
|
attr_reader :main_branch, :protected_branches, :hooks_path, :managed_hooks,
|
|
12
|
-
:template_managed_files, :template_canonical,
|
|
27
|
+
:template_managed_files, :lint_canonical, :template_canonical,
|
|
13
28
|
:review_wait_seconds, :review_poll_seconds, :review_max_polls, :review_sweep_window_days,
|
|
14
29
|
:review_sweep_states, :review_disposition, :review_risk_keywords,
|
|
15
30
|
:review_tracking_issue_title, :review_tracking_issue_label, :review_bot_usernames,
|
|
@@ -38,7 +53,10 @@ module Carson
|
|
|
38
53
|
"managed" => [ "pre-commit", "prepare-commit-msg", "pre-merge-commit", "pre-push" ]
|
|
39
54
|
},
|
|
40
55
|
"template" => {
|
|
41
|
-
"managed_files" => [
|
|
56
|
+
"managed_files" => [],
|
|
57
|
+
"canonical" => nil
|
|
58
|
+
},
|
|
59
|
+
"lint" => {
|
|
42
60
|
"canonical" => nil
|
|
43
61
|
},
|
|
44
62
|
"workflow" => {
|
|
@@ -66,7 +84,9 @@ module Carson
|
|
|
66
84
|
"govern" => {
|
|
67
85
|
"repos" => [],
|
|
68
86
|
"auto_merge" => true,
|
|
69
|
-
"
|
|
87
|
+
"merge" => {
|
|
88
|
+
"method" => "squash"
|
|
89
|
+
},
|
|
70
90
|
"agent" => {
|
|
71
91
|
"provider" => "auto",
|
|
72
92
|
"codex" => {},
|
|
@@ -154,7 +174,10 @@ module Carson
|
|
|
154
174
|
govern_auto_merge = ENV.fetch( "CARSON_GOVERN_AUTO_MERGE", "" ).to_s.strip
|
|
155
175
|
govern[ "auto_merge" ] = ( govern_auto_merge == "true" ) unless govern_auto_merge.empty?
|
|
156
176
|
govern_method = ENV.fetch( "CARSON_GOVERN_MERGE_METHOD", "" ).to_s.strip
|
|
157
|
-
|
|
177
|
+
unless govern_method.empty?
|
|
178
|
+
govern[ "merge" ] ||= {}
|
|
179
|
+
govern[ "merge" ][ "method" ] = govern_method
|
|
180
|
+
end
|
|
158
181
|
agent = fetch_hash_section( data: govern, key: "agent" )
|
|
159
182
|
govern_provider = ENV.fetch( "CARSON_GOVERN_AGENT_PROVIDER", "" ).to_s.strip
|
|
160
183
|
agent[ "provider" ] = govern_provider unless govern_provider.empty?
|
|
@@ -189,8 +212,11 @@ module Carson
|
|
|
189
212
|
@hooks_path = fetch_string( hash: fetch_hash( hash: data, key: "hooks" ), key: "path" )
|
|
190
213
|
@managed_hooks = fetch_string_array( hash: fetch_hash( hash: data, key: "hooks" ), key: "managed" )
|
|
191
214
|
|
|
192
|
-
|
|
193
|
-
@
|
|
215
|
+
template_hash = fetch_hash( hash: data, key: "template" )
|
|
216
|
+
@template_managed_files = fetch_optional_string_array( hash: template_hash, key: "managed_files" )
|
|
217
|
+
@lint_canonical = fetch_optional_path( hash: fetch_hash( hash: data, key: "lint" ), key: "canonical" )
|
|
218
|
+
@lint_canonical ||= fetch_optional_path( hash: template_hash, key: "canonical" )
|
|
219
|
+
@template_canonical = @lint_canonical
|
|
194
220
|
resolve_canonical_files!
|
|
195
221
|
|
|
196
222
|
workflow_hash = fetch_hash( hash: data, key: "workflow" )
|
|
@@ -215,7 +241,8 @@ module Carson
|
|
|
215
241
|
govern_hash = fetch_hash( hash: data, key: "govern" )
|
|
216
242
|
@govern_repos = fetch_optional_string_array( hash: govern_hash, key: "repos" ).map { |path| safe_expand_path( path ) }
|
|
217
243
|
@govern_auto_merge = fetch_optional_boolean( hash: govern_hash, key: "auto_merge", default: true, key_path: "govern.auto_merge" )
|
|
218
|
-
|
|
244
|
+
govern_merge_hash = fetch_hash( hash: govern_hash, key: "merge" )
|
|
245
|
+
@govern_merge_method = fetch_string( hash: govern_merge_hash, key: "method" ).downcase
|
|
219
246
|
govern_agent_hash = fetch_hash( hash: govern_hash, key: "agent" )
|
|
220
247
|
@govern_agent_provider = fetch_string( hash: govern_agent_hash, key: "provider" ).downcase
|
|
221
248
|
dispatch_path = govern_hash.fetch( "dispatch_state_path" ).to_s
|
|
@@ -317,18 +344,32 @@ module Carson
|
|
|
317
344
|
safe_expand_path( text )
|
|
318
345
|
end
|
|
319
346
|
|
|
320
|
-
# Discovers files in the canonical directory and appends them to managed_files.
|
|
321
|
-
#
|
|
347
|
+
# Discovers files in the canonical lint-policy directory and appends them to managed_files.
|
|
348
|
+
# Explicit GitHub paths stay under .github/; flat policy files default to .github/linters/.
|
|
322
349
|
def resolve_canonical_files!
|
|
323
|
-
return if @
|
|
324
|
-
return unless Dir.exist?( @
|
|
350
|
+
return if @lint_canonical.nil? || @lint_canonical.empty?
|
|
351
|
+
return unless Dir.exist?( @lint_canonical )
|
|
325
352
|
|
|
326
|
-
Dir.glob( File.join( @
|
|
353
|
+
Dir.glob( File.join( @lint_canonical, "**", "*" ), File::FNM_DOTMATCH ).sort.each do |absolute_path|
|
|
354
|
+
basename = File.basename( absolute_path )
|
|
355
|
+
next if basename == "." || basename == ".."
|
|
327
356
|
next unless File.file?( absolute_path )
|
|
328
|
-
relative = absolute_path.delete_prefix( "#{@
|
|
329
|
-
managed_path =
|
|
357
|
+
relative = absolute_path.delete_prefix( "#{@lint_canonical}/" )
|
|
358
|
+
managed_path = canonical_managed_path( relative_path: relative )
|
|
330
359
|
@template_managed_files << managed_path unless @template_managed_files.include?( managed_path )
|
|
331
360
|
end
|
|
332
361
|
end
|
|
362
|
+
|
|
363
|
+
def canonical_managed_path( relative_path: )
|
|
364
|
+
raw_relative = relative_path.to_s
|
|
365
|
+
return ".github/#{raw_relative.delete_prefix( '.github/' )}" if raw_relative.start_with?( ".github/" )
|
|
366
|
+
|
|
367
|
+
clean_relative = raw_relative
|
|
368
|
+
first_segment = clean_relative.split( "/", 2 ).first
|
|
369
|
+
return ".github/#{clean_relative}" if CANONICAL_GITHUB_DIRECTORIES.include?( first_segment )
|
|
370
|
+
return ".github/#{clean_relative}" if !clean_relative.include?( "/" ) && CANONICAL_GITHUB_ROOT_FILES.include?( clean_relative )
|
|
371
|
+
|
|
372
|
+
".github/linters/#{clean_relative}"
|
|
373
|
+
end
|
|
333
374
|
end
|
|
334
375
|
end
|
data/lib/carson/runtime/audit.rb
CHANGED
|
@@ -26,6 +26,12 @@ module Carson
|
|
|
26
26
|
puts_verbose ""
|
|
27
27
|
puts_verbose "[Working Tree]"
|
|
28
28
|
puts_verbose git_capture!( "status", "--short", "--branch" ).strip
|
|
29
|
+
working_tree = audit_working_tree_report
|
|
30
|
+
if working_tree.fetch( :status ) == "block"
|
|
31
|
+
puts_verbose "ACTION: #{working_tree.fetch( :error )}; #{working_tree.fetch( :recovery )}."
|
|
32
|
+
audit_state = "block"
|
|
33
|
+
audit_concise_problems << "Working tree: #{working_tree.fetch( :error )} — #{working_tree.fetch( :recovery )}."
|
|
34
|
+
end
|
|
29
35
|
puts_verbose ""
|
|
30
36
|
puts_verbose "[Hooks]"
|
|
31
37
|
hooks_ok = hooks_health_report
|
|
@@ -110,17 +116,17 @@ module Carson
|
|
|
110
116
|
end
|
|
111
117
|
audit_concise_problems << "Baseline (#{default_branch_baseline.fetch( :default_branch, config.main_branch )}): #{parts.join( ', ' )}."
|
|
112
118
|
end
|
|
113
|
-
if config.
|
|
119
|
+
if config.lint_canonical.nil? || config.lint_canonical.to_s.empty?
|
|
114
120
|
puts_verbose ""
|
|
115
|
-
puts_verbose "[Canonical
|
|
116
|
-
puts_verbose "HINT: canonical
|
|
121
|
+
puts_verbose "[Canonical Lint Policy]"
|
|
122
|
+
puts_verbose "HINT: lint.canonical not configured — run carson setup to enable."
|
|
117
123
|
end
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
)
|
|
124
|
+
write_and_print_pr_monitor_report(
|
|
125
|
+
report: monitor_report.merge(
|
|
126
|
+
default_branch_baseline: default_branch_baseline,
|
|
127
|
+
audit_status: audit_state
|
|
123
128
|
)
|
|
129
|
+
)
|
|
124
130
|
exit_code = audit_state == "block" ? EXIT_BLOCK : EXIT_OK
|
|
125
131
|
|
|
126
132
|
if json_output
|
|
@@ -128,6 +134,7 @@ module Carson
|
|
|
128
134
|
command: "audit",
|
|
129
135
|
status: audit_state,
|
|
130
136
|
branch: current_branch,
|
|
137
|
+
working_tree: working_tree,
|
|
131
138
|
hooks: { status: hooks_status },
|
|
132
139
|
main_sync: main_sync,
|
|
133
140
|
pr: monitor_report[ :pr ],
|
|
@@ -175,7 +182,7 @@ module Carson
|
|
|
175
182
|
repos.each do |repo_path|
|
|
176
183
|
repo_name = File.basename( repo_path )
|
|
177
184
|
unless Dir.exist?( repo_path )
|
|
178
|
-
puts_line "#{repo_name}:
|
|
185
|
+
puts_line "#{repo_name}: not found"
|
|
179
186
|
record_batch_skip( command: "audit", repo_path: repo_path, reason: "path not found" )
|
|
180
187
|
failed += 1
|
|
181
188
|
next
|
|
@@ -190,15 +197,15 @@ module Carson
|
|
|
190
197
|
clear_batch_success( command: "audit", repo_path: repo_path )
|
|
191
198
|
passed += 1
|
|
192
199
|
when EXIT_BLOCK
|
|
193
|
-
puts_line "#{repo_name}:
|
|
200
|
+
puts_line "#{repo_name}: needs attention" unless verbose?
|
|
194
201
|
blocked += 1
|
|
195
202
|
else
|
|
196
|
-
puts_line "#{repo_name}:
|
|
203
|
+
puts_line "#{repo_name}: could not complete" unless verbose?
|
|
197
204
|
record_batch_skip( command: "audit", repo_path: repo_path, reason: "audit failed" )
|
|
198
205
|
failed += 1
|
|
199
206
|
end
|
|
200
207
|
rescue StandardError => exception
|
|
201
|
-
puts_line "#{repo_name}:
|
|
208
|
+
puts_line "#{repo_name}: could not complete (#{exception.message})"
|
|
202
209
|
record_batch_skip( command: "audit", repo_path: repo_path, reason: exception.message )
|
|
203
210
|
failed += 1
|
|
204
211
|
end
|
|
@@ -207,11 +214,30 @@ module Carson
|
|
|
207
214
|
puts_line ""
|
|
208
215
|
puts_line "Audit all complete: #{passed} ok, #{blocked} blocked, #{failed} failed."
|
|
209
216
|
blocked.zero? && failed.zero? ? EXIT_OK : EXIT_BLOCK
|
|
210
|
-
|
|
217
|
+
end
|
|
211
218
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
219
|
+
# rubocop:disable Layout/AccessModifierIndentation -- tab-width calculation produces unfixable mixed tabs+spaces
|
|
220
|
+
private
|
|
221
|
+
# rubocop:enable Layout/AccessModifierIndentation
|
|
222
|
+
def audit_working_tree_report
|
|
223
|
+
dirty_reason = dirty_worktree_reason
|
|
224
|
+
return { dirty: false, context: nil, status: "ok" } if dirty_reason.nil?
|
|
225
|
+
|
|
226
|
+
if dirty_reason == "main_worktree"
|
|
227
|
+
{
|
|
228
|
+
dirty: true,
|
|
229
|
+
context: dirty_reason,
|
|
230
|
+
status: "block",
|
|
231
|
+
error: "main working tree has uncommitted changes",
|
|
232
|
+
recovery: "create a worktree with carson worktree create <name>"
|
|
233
|
+
}
|
|
234
|
+
else
|
|
235
|
+
{ dirty: true, context: dirty_reason, status: "ok" }
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def pr_and_check_report
|
|
240
|
+
report = {
|
|
215
241
|
generated_at: Time.now.utc.iso8601,
|
|
216
242
|
branch: current_branch,
|
|
217
243
|
status: "ok",
|
|
@@ -581,7 +607,6 @@ module Carson
|
|
|
581
607
|
lines.join( "\n" )
|
|
582
608
|
end
|
|
583
609
|
|
|
584
|
-
# True when there are no staged/unstaged/untracked file changes.
|
|
585
610
|
end
|
|
586
611
|
|
|
587
612
|
include Audit
|