carson 2.24.0 → 2.26.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/.github/workflows/carson_policy.yml +5 -14
- data/API.md +6 -36
- data/MANUAL.md +10 -38
- data/README.md +9 -22
- data/RELEASE.md +24 -0
- data/SKILL.md +5 -7
- data/VERSION +1 -1
- data/lib/carson/cli.rb +1 -59
- data/lib/carson/config.rb +25 -50
- data/lib/carson/runtime/audit.rb +2 -61
- data/lib/carson/runtime/govern.rb +7 -36
- data/lib/carson/runtime/local/hooks.rb +142 -0
- data/lib/carson/runtime/local/onboard.rb +356 -0
- data/lib/carson/runtime/local/prune.rb +292 -0
- data/lib/carson/runtime/local/sync.rb +93 -0
- data/lib/carson/runtime/local/template.rb +347 -0
- data/lib/carson/runtime/local.rb +6 -1225
- data/lib/carson/runtime/review/gate_support.rb +1 -1
- data/lib/carson/runtime/review/sweep_support.rb +1 -1
- data/lib/carson/runtime/review/utility.rb +2 -2
- data/lib/carson/runtime/setup.rb +8 -2
- data/lib/carson/runtime.rb +0 -1
- data/templates/.github/carson.md +0 -1
- metadata +6 -2
- data/lib/carson/runtime/lint.rb +0 -154
|
@@ -167,7 +167,7 @@ module Carson
|
|
|
167
167
|
|
|
168
168
|
# Writes review gate artefacts using fixed report names in global report output.
|
|
169
169
|
def write_review_gate_report( report: )
|
|
170
|
-
markdown_path, json_path =
|
|
170
|
+
markdown_path, json_path = report(
|
|
171
171
|
report: report,
|
|
172
172
|
markdown_name: REVIEW_GATE_REPORT_MD,
|
|
173
173
|
json_name: REVIEW_GATE_REPORT_JSON,
|
|
@@ -199,7 +199,7 @@ module Carson
|
|
|
199
199
|
|
|
200
200
|
# Writes sweep artefacts for CI logs and local troubleshooting.
|
|
201
201
|
def write_review_sweep_report( report: )
|
|
202
|
-
markdown_path, json_path =
|
|
202
|
+
markdown_path, json_path = report(
|
|
203
203
|
report: report,
|
|
204
204
|
markdown_name: REVIEW_SWEEP_REPORT_MD,
|
|
205
205
|
json_name: REVIEW_SWEEP_REPORT_JSON,
|
|
@@ -14,7 +14,7 @@ module Carson
|
|
|
14
14
|
|
|
15
15
|
# Disposition records always start with configured prefix.
|
|
16
16
|
def disposition_prefixed?( text: )
|
|
17
|
-
text.to_s.lstrip.start_with?( config.
|
|
17
|
+
text.to_s.lstrip.start_with?( config.review_disposition )
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
# Extracts first matching disposition token from configured acknowledgement body.
|
|
@@ -48,7 +48,7 @@ module Carson
|
|
|
48
48
|
end
|
|
49
49
|
|
|
50
50
|
# Shared report writer for JSON plus Markdown pairs in global report output.
|
|
51
|
-
def
|
|
51
|
+
def report( report:, markdown_name:, json_name:, renderer: )
|
|
52
52
|
report_dir = report_dir_path
|
|
53
53
|
FileUtils.mkdir_p( report_dir )
|
|
54
54
|
markdown_path = File.join( report_dir, markdown_name )
|
data/lib/carson/runtime/setup.rb
CHANGED
|
@@ -127,22 +127,28 @@ module Carson
|
|
|
127
127
|
def prompt_workflow_style
|
|
128
128
|
puts_line ""
|
|
129
129
|
puts_line "Workflow style"
|
|
130
|
+
current = config.workflow_style
|
|
131
|
+
puts_line " Currently: #{current}" unless current.nil? || current.empty?
|
|
130
132
|
options = [
|
|
131
133
|
{ label: "branch — enforce PR-only merges (default)", value: "branch" },
|
|
132
134
|
{ label: "trunk — commit directly to main", value: "trunk" }
|
|
133
135
|
]
|
|
134
|
-
|
|
136
|
+
default_index = options.index { |o| o.fetch( :value ) == current } || 0
|
|
137
|
+
prompt_choice( options: options, default: default_index )
|
|
135
138
|
end
|
|
136
139
|
|
|
137
140
|
def prompt_merge_method
|
|
138
141
|
puts_line ""
|
|
139
142
|
puts_line "Merge method"
|
|
143
|
+
current = config.govern_merge_method
|
|
144
|
+
puts_line " Currently: #{current}" unless current.nil? || current.empty?
|
|
140
145
|
options = [
|
|
141
146
|
{ label: "squash — one commit per PR (recommended)", value: "squash" },
|
|
142
147
|
{ label: "rebase — linear history, individual commits", value: "rebase" },
|
|
143
148
|
{ label: "merge — merge commits", value: "merge" }
|
|
144
149
|
]
|
|
145
|
-
|
|
150
|
+
default_index = options.index { |o| o.fetch( :value ) == current } || 0
|
|
151
|
+
prompt_choice( options: options, default: default_index )
|
|
146
152
|
end
|
|
147
153
|
|
|
148
154
|
def prompt_canonical_template
|
data/lib/carson/runtime.rb
CHANGED
data/templates/.github/carson.md
CHANGED
|
@@ -24,7 +24,6 @@ carson review gate # block until actionable review findings are resolved
|
|
|
24
24
|
```bash
|
|
25
25
|
carson sync # fast-forward local main from remote
|
|
26
26
|
carson prune # remove stale branches (safer than git branch -d on squash repos)
|
|
27
|
-
carson housekeep # sync + prune together
|
|
28
27
|
```
|
|
29
28
|
|
|
30
29
|
## Exit Codes
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: carson
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.26.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Hailei Wang
|
|
@@ -53,8 +53,12 @@ files:
|
|
|
53
53
|
- lib/carson/runtime.rb
|
|
54
54
|
- lib/carson/runtime/audit.rb
|
|
55
55
|
- lib/carson/runtime/govern.rb
|
|
56
|
-
- lib/carson/runtime/lint.rb
|
|
57
56
|
- lib/carson/runtime/local.rb
|
|
57
|
+
- lib/carson/runtime/local/hooks.rb
|
|
58
|
+
- lib/carson/runtime/local/onboard.rb
|
|
59
|
+
- lib/carson/runtime/local/prune.rb
|
|
60
|
+
- lib/carson/runtime/local/sync.rb
|
|
61
|
+
- lib/carson/runtime/local/template.rb
|
|
58
62
|
- lib/carson/runtime/review.rb
|
|
59
63
|
- lib/carson/runtime/review/data_access.rb
|
|
60
64
|
- lib/carson/runtime/review/gate_support.rb
|
data/lib/carson/runtime/lint.rb
DELETED
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
require "fileutils"
|
|
2
|
-
require "open3"
|
|
3
|
-
require "tmpdir"
|
|
4
|
-
|
|
5
|
-
module Carson
|
|
6
|
-
class Runtime
|
|
7
|
-
module Lint
|
|
8
|
-
# Distributes lint policy files from a central source into the governed repository.
|
|
9
|
-
# Target: <repo>/.github/linters/ (MegaLinter auto-discovers here).
|
|
10
|
-
def lint_setup!( source:, ref: "main", force: false, **_ )
|
|
11
|
-
puts_verbose ""
|
|
12
|
-
puts_verbose "[Lint Policy]"
|
|
13
|
-
source_text = source.to_s.strip
|
|
14
|
-
if source_text.empty?
|
|
15
|
-
puts_line "ERROR: lint policy requires --source <path-or-git-url>."
|
|
16
|
-
return EXIT_ERROR
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
ref_text = ref.to_s.strip
|
|
20
|
-
ref_text = "main" if ref_text.empty?
|
|
21
|
-
source_dir, cleanup = lint_setup_source_directory( source: source_text, ref: ref_text )
|
|
22
|
-
begin
|
|
23
|
-
target_dir = repo_linters_dir
|
|
24
|
-
copy_result = copy_lint_policy_files(
|
|
25
|
-
source_dir: source_dir,
|
|
26
|
-
target_dir: target_dir,
|
|
27
|
-
force: force
|
|
28
|
-
)
|
|
29
|
-
puts_verbose "lint_policy_source: #{source_text}"
|
|
30
|
-
puts_verbose "lint_policy_ref: #{ref_text}" if lint_source_git_url?( source: source_text )
|
|
31
|
-
puts_verbose "lint_policy_target: #{target_dir}"
|
|
32
|
-
puts_verbose "lint_policy_created: #{copy_result.fetch( :created )}"
|
|
33
|
-
puts_verbose "lint_policy_updated: #{copy_result.fetch( :updated )}"
|
|
34
|
-
puts_verbose "lint_policy_skipped: #{copy_result.fetch( :skipped )}"
|
|
35
|
-
|
|
36
|
-
puts_line "OK: lint policy synced to .github/linters/ (#{copy_result.fetch( :created )} created, #{copy_result.fetch( :updated )} updated)."
|
|
37
|
-
EXIT_OK
|
|
38
|
-
ensure
|
|
39
|
-
cleanup&.call
|
|
40
|
-
end
|
|
41
|
-
rescue StandardError => e
|
|
42
|
-
puts_line "ERROR: lint policy failed (#{e.message})"
|
|
43
|
-
EXIT_ERROR
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
private
|
|
47
|
-
def lint_setup_source_directory( source:, ref: )
|
|
48
|
-
if lint_source_git_url?( source: source )
|
|
49
|
-
return lint_setup_clone_source( source: source, ref: ref )
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
expanded_source = File.expand_path( source )
|
|
53
|
-
raise "source path does not exist: #{expanded_source}" unless Dir.exist?( expanded_source )
|
|
54
|
-
[ expanded_source, nil ]
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def lint_source_git_url?( source: )
|
|
58
|
-
text = source.to_s.strip
|
|
59
|
-
text.start_with?( "https://", "http://", "ssh://", "git@", "file://" )
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
def lint_setup_clone_source( source:, ref: )
|
|
63
|
-
cache_root = cache_workspace_root
|
|
64
|
-
FileUtils.mkdir_p( cache_root )
|
|
65
|
-
work_dir = Dir.mktmpdir( "carson-lint-setup-", cache_root )
|
|
66
|
-
checkout_dir = File.join( work_dir, "source" )
|
|
67
|
-
clone_source = authenticated_lint_source( source: source )
|
|
68
|
-
stdout_text, stderr_text, status = Open3.capture3(
|
|
69
|
-
"git", "clone", "--depth", "1", "--branch", ref, clone_source, checkout_dir
|
|
70
|
-
)
|
|
71
|
-
unless status.success?
|
|
72
|
-
error_text = [ stderr_text.to_s.strip, stdout_text.to_s.strip ].reject( &:empty? ).join( " | " )
|
|
73
|
-
error_text = "git clone failed" if error_text.empty?
|
|
74
|
-
raise "unable to clone lint source #{safe_lint_source( source: source )} (#{error_text})"
|
|
75
|
-
end
|
|
76
|
-
[ checkout_dir, -> { FileUtils.rm_rf( work_dir ) } ]
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
def authenticated_lint_source( source: )
|
|
80
|
-
token = ENV.fetch( "CARSON_READ_TOKEN", "" ).to_s.strip
|
|
81
|
-
return source if token.empty?
|
|
82
|
-
|
|
83
|
-
return source unless source.start_with?( "https://github.com/", "http://github.com/", "git@github.com:" )
|
|
84
|
-
|
|
85
|
-
if source.start_with?( "git@github.com:" )
|
|
86
|
-
path = source.sub( "git@github.com:", "" )
|
|
87
|
-
return "https://x-access-token:#{token}@github.com/#{path}"
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
source.sub( %r{\Ahttps?://github\.com/}, "https://x-access-token:#{token}@github.com/" )
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
def safe_lint_source( source: )
|
|
94
|
-
source.to_s.gsub( %r{https://[^@]+@}, "https://***@" )
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
def cache_workspace_root
|
|
98
|
-
home = ENV.fetch( "HOME", "" ).to_s.strip
|
|
99
|
-
if home.start_with?( "/" )
|
|
100
|
-
path = File.join( home, ".carson", "cache" )
|
|
101
|
-
FileUtils.mkdir_p( path )
|
|
102
|
-
return path
|
|
103
|
-
end
|
|
104
|
-
"/tmp/carson"
|
|
105
|
-
rescue StandardError
|
|
106
|
-
"/tmp/carson"
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
# Lint configs live inside the governed repository for MegaLinter.
|
|
110
|
-
def repo_linters_dir
|
|
111
|
-
File.join( repo_root, ".github", "linters" )
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
def copy_lint_policy_files( source_dir:, target_dir:, force: )
|
|
115
|
-
FileUtils.mkdir_p( target_dir )
|
|
116
|
-
created = 0
|
|
117
|
-
updated = 0
|
|
118
|
-
skipped = 0
|
|
119
|
-
Dir.glob( "**/*", File::FNM_DOTMATCH, base: source_dir ).sort.each do |relative|
|
|
120
|
-
next if [ ".", ".." ].include?( relative )
|
|
121
|
-
next if relative.start_with?( ".git/" ) || relative == ".git"
|
|
122
|
-
source_path = File.join( source_dir, relative )
|
|
123
|
-
target_path = File.join( target_dir, relative )
|
|
124
|
-
if File.directory?( source_path )
|
|
125
|
-
FileUtils.mkdir_p( target_path )
|
|
126
|
-
next
|
|
127
|
-
end
|
|
128
|
-
next unless File.file?( source_path )
|
|
129
|
-
|
|
130
|
-
if File.exist?( target_path ) && !force
|
|
131
|
-
skipped += 1
|
|
132
|
-
next
|
|
133
|
-
end
|
|
134
|
-
target_exists = File.exist?( target_path )
|
|
135
|
-
FileUtils.mkdir_p( File.dirname( target_path ) )
|
|
136
|
-
FileUtils.cp( source_path, target_path )
|
|
137
|
-
FileUtils.chmod( File.stat( source_path ).mode & 0o777, target_path )
|
|
138
|
-
if target_exists
|
|
139
|
-
updated += 1
|
|
140
|
-
else
|
|
141
|
-
created += 1
|
|
142
|
-
end
|
|
143
|
-
end
|
|
144
|
-
{
|
|
145
|
-
created: created,
|
|
146
|
-
updated: updated,
|
|
147
|
-
skipped: skipped
|
|
148
|
-
}
|
|
149
|
-
end
|
|
150
|
-
end
|
|
151
|
-
|
|
152
|
-
include Lint
|
|
153
|
-
end
|
|
154
|
-
end
|