carson 2.14.2 → 2.15.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/VERSION +1 -1
- data/hooks/pre-push +10 -0
- data/lib/carson/cli.rb +34 -3
- data/lib/carson/runtime/audit.rb +54 -0
- data/lib/carson/runtime/local.rb +32 -2
- 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: e14a15b6d3d91c0bbf75d0b3652a65ee7339b2c2cdeb56cd0937efe94f90c584
|
|
4
|
+
data.tar.gz: 1b08caaaf7a4e453ce55256eca2c961b09ffaaae761a6be101e10e3ab0f61397
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d594bcef84c8aa142438ef8b625c71f38366bb19e4a7a8abc48b415b882946b93845d226cac1eecc08a316b94677b8bcdf02edcdb9eac70a55f1f85fd800922b
|
|
7
|
+
data.tar.gz: 02ea0cc8507330c1dbb8e7e1b6a1f8e4f3a88f0d9aa396b296251cbd70fe916f4fcdb7b564301427d6911052376071f35098e34021c19cb6c28e7d05c84a0f79
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
2.
|
|
1
|
+
2.15.0
|
data/hooks/pre-push
CHANGED
|
@@ -7,6 +7,7 @@ style="$(cat "$hooks_dir/workflow_style" 2>/dev/null || echo "branch")"
|
|
|
7
7
|
|
|
8
8
|
remote_name="${1:-unknown}"
|
|
9
9
|
remote_url="${2:-unknown}"
|
|
10
|
+
has_commit_push=false
|
|
10
11
|
while read -r local_ref local_sha remote_ref remote_sha; do
|
|
11
12
|
case "$remote_ref" in
|
|
12
13
|
refs/heads/main|refs/heads/master)
|
|
@@ -14,4 +15,13 @@ while read -r local_ref local_sha remote_ref remote_sha; do
|
|
|
14
15
|
exit 1
|
|
15
16
|
;;
|
|
16
17
|
esac
|
|
18
|
+
[[ "$local_sha" != "0000000000000000000000000000000000000000" ]] && has_commit_push=true
|
|
17
19
|
done
|
|
20
|
+
|
|
21
|
+
if $has_commit_push; then
|
|
22
|
+
if [[ -n "${CARSON_BIN:-}" ]]; then
|
|
23
|
+
ruby "$CARSON_BIN" template apply --push-prep || exit 1
|
|
24
|
+
else
|
|
25
|
+
carson template apply --push-prep || exit 1
|
|
26
|
+
fi
|
|
27
|
+
fi
|
data/lib/carson/cli.rb
CHANGED
|
@@ -53,7 +53,7 @@ module Carson
|
|
|
53
53
|
|
|
54
54
|
def self.build_parser
|
|
55
55
|
OptionParser.new do |opts|
|
|
56
|
-
opts.banner = "Usage: carson [setup|audit|sync|prune|prepare|inspect|onboard [repo_path]|refresh [--all|repo_path]|offboard [repo_path]|template check|template apply|lint policy --source <path-or-git-url>|review gate|review sweep|govern [--dry-run] [--json] [--loop SECONDS]|housekeep|version]"
|
|
56
|
+
opts.banner = "Usage: carson [setup|audit|check|sync|prune|prepare|inspect|onboard [repo_path]|refresh [--all|repo_path]|offboard [repo_path]|template check|template apply|lint policy --source <path-or-git-url>|review gate|review sweep|govern [--dry-run] [--json] [--loop SECONDS]|housekeep|version]"
|
|
57
57
|
end
|
|
58
58
|
end
|
|
59
59
|
|
|
@@ -79,7 +79,7 @@ module Carson
|
|
|
79
79
|
when "refresh"
|
|
80
80
|
parse_refresh_command( argv: argv, parser: parser, err: err )
|
|
81
81
|
when "template"
|
|
82
|
-
|
|
82
|
+
parse_template_subcommand( argv: argv, parser: parser, err: err )
|
|
83
83
|
when "lint"
|
|
84
84
|
parse_lint_subcommand( argv: argv, parser: parser, err: err )
|
|
85
85
|
when "review"
|
|
@@ -143,6 +143,35 @@ module Carson
|
|
|
143
143
|
{ command: "#{command}:#{action}" }
|
|
144
144
|
end
|
|
145
145
|
|
|
146
|
+
def self.parse_template_subcommand( argv:, parser:, err: )
|
|
147
|
+
action = argv.shift
|
|
148
|
+
if action.to_s.strip.empty?
|
|
149
|
+
err.puts "#{BADGE} Missing subcommand for template. Use: carson template check|apply"
|
|
150
|
+
err.puts parser
|
|
151
|
+
return { command: :invalid }
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
return { command: "template:#{action}" } unless action == "apply"
|
|
155
|
+
|
|
156
|
+
options = { push_prep: false }
|
|
157
|
+
apply_parser = OptionParser.new do |opts|
|
|
158
|
+
opts.banner = "Usage: carson template apply [--push-prep]"
|
|
159
|
+
opts.on( "--push-prep", "Apply templates and auto-commit any managed file changes (used by pre-push hook)" ) do
|
|
160
|
+
options[ :push_prep ] = true
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
apply_parser.parse!( argv )
|
|
164
|
+
unless argv.empty?
|
|
165
|
+
err.puts "#{BADGE} Unexpected arguments for template apply: #{argv.join( ' ' )}"
|
|
166
|
+
err.puts apply_parser
|
|
167
|
+
return { command: :invalid }
|
|
168
|
+
end
|
|
169
|
+
{ command: "template:apply", push_prep: options.fetch( :push_prep ) }
|
|
170
|
+
rescue OptionParser::ParseError => e
|
|
171
|
+
err.puts "#{BADGE} #{e.message}"
|
|
172
|
+
{ command: :invalid }
|
|
173
|
+
end
|
|
174
|
+
|
|
146
175
|
def self.parse_lint_subcommand( argv:, parser:, err: )
|
|
147
176
|
action = argv.shift
|
|
148
177
|
unless action == "policy"
|
|
@@ -235,6 +264,8 @@ module Carson
|
|
|
235
264
|
runtime.prepare!
|
|
236
265
|
when "inspect"
|
|
237
266
|
runtime.inspect!
|
|
267
|
+
when "check"
|
|
268
|
+
runtime.check!
|
|
238
269
|
when "onboard"
|
|
239
270
|
runtime.onboard!
|
|
240
271
|
when "refresh"
|
|
@@ -246,7 +277,7 @@ module Carson
|
|
|
246
277
|
when "template:check"
|
|
247
278
|
runtime.template_check!
|
|
248
279
|
when "template:apply"
|
|
249
|
-
runtime.template_apply!
|
|
280
|
+
runtime.template_apply!( push_prep: parsed.fetch( :push_prep, false ) )
|
|
250
281
|
when "lint:setup"
|
|
251
282
|
runtime.lint_setup!(
|
|
252
283
|
source: parsed.fetch( :source ),
|
data/lib/carson/runtime/audit.rb
CHANGED
|
@@ -74,6 +74,60 @@ module Carson
|
|
|
74
74
|
audit_state == "block" ? EXIT_BLOCK : EXIT_OK
|
|
75
75
|
end
|
|
76
76
|
|
|
77
|
+
# Thin focused command: show required-check status for the current branch's open PR.
|
|
78
|
+
# Always exits 0 for pending or passing so callers never see a false "Error: Exit code 8".
|
|
79
|
+
def check!
|
|
80
|
+
unless gh_available?
|
|
81
|
+
puts_line "Checks: gh CLI not available."
|
|
82
|
+
return EXIT_ERROR
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
pr_stdout, pr_stderr, pr_success, = gh_run(
|
|
86
|
+
"pr", "view", current_branch,
|
|
87
|
+
"--json", "number,title,url"
|
|
88
|
+
)
|
|
89
|
+
unless pr_success
|
|
90
|
+
error_text = gh_error_text( stdout_text: pr_stdout, stderr_text: pr_stderr, fallback: "no open PR for branch #{current_branch}" )
|
|
91
|
+
puts_line "Checks: #{error_text}."
|
|
92
|
+
return EXIT_ERROR
|
|
93
|
+
end
|
|
94
|
+
pr_data = JSON.parse( pr_stdout )
|
|
95
|
+
pr_number = pr_data[ "number" ].to_s
|
|
96
|
+
|
|
97
|
+
checks_stdout, checks_stderr, checks_success, checks_exit = gh_run(
|
|
98
|
+
"pr", "checks", pr_number, "--required", "--json", "name,state,bucket,workflow,link"
|
|
99
|
+
)
|
|
100
|
+
if checks_stdout.to_s.strip.empty?
|
|
101
|
+
error_text = gh_error_text( stdout_text: checks_stdout, stderr_text: checks_stderr, fallback: "required checks unavailable" )
|
|
102
|
+
puts_line "Checks: #{error_text}."
|
|
103
|
+
return EXIT_ERROR
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
checks_data = JSON.parse( checks_stdout )
|
|
107
|
+
failing = checks_data.select { |e| e[ "bucket" ].to_s == "fail" || e[ "state" ].to_s.upcase == "FAILURE" }
|
|
108
|
+
pending = checks_data.select { |e| e[ "bucket" ].to_s == "pending" }
|
|
109
|
+
total = checks_data.count
|
|
110
|
+
# gh exits 8 when required checks are still pending (not a failure).
|
|
111
|
+
is_pending = !checks_success && checks_exit == 8
|
|
112
|
+
|
|
113
|
+
if failing.any?
|
|
114
|
+
puts_line "Checks: FAIL (#{failing.count} of #{total} failing)."
|
|
115
|
+
normalise_check_entries( entries: failing ).each { |e| puts_line " #{e.fetch( :workflow )} / #{e.fetch( :name )} #{e.fetch( :link )}".strip }
|
|
116
|
+
return EXIT_BLOCK
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
if is_pending || pending.any?
|
|
120
|
+
puts_line "Checks: pending (#{total - pending.count} of #{total} complete)."
|
|
121
|
+
return EXIT_OK
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
puts_line "Checks: all passing (#{total} required)."
|
|
125
|
+
EXIT_OK
|
|
126
|
+
rescue JSON::ParserError => e
|
|
127
|
+
puts_line "Checks: invalid gh response (#{e.message})."
|
|
128
|
+
EXIT_ERROR
|
|
129
|
+
end
|
|
130
|
+
|
|
77
131
|
private
|
|
78
132
|
def pr_and_check_report
|
|
79
133
|
report = {
|
data/lib/carson/runtime/local.rb
CHANGED
|
@@ -367,7 +367,7 @@ module Carson
|
|
|
367
367
|
|
|
368
368
|
# Applies managed template files as full-file writes from Carson sources.
|
|
369
369
|
# Also removes superseded files that are no longer part of the managed set.
|
|
370
|
-
def template_apply!
|
|
370
|
+
def template_apply!( push_prep: false )
|
|
371
371
|
fingerprint_status = block_if_outsider_fingerprints!
|
|
372
372
|
return fingerprint_status unless fingerprint_status.nil?
|
|
373
373
|
|
|
@@ -414,7 +414,10 @@ module Carson
|
|
|
414
414
|
puts_line "Templates in sync."
|
|
415
415
|
end
|
|
416
416
|
end
|
|
417
|
-
error_count.positive?
|
|
417
|
+
return EXIT_ERROR if error_count.positive?
|
|
418
|
+
|
|
419
|
+
push_prep_commit! if push_prep
|
|
420
|
+
EXIT_OK
|
|
418
421
|
end
|
|
419
422
|
|
|
420
423
|
private
|
|
@@ -742,6 +745,33 @@ module Carson
|
|
|
742
745
|
git_capture!( "status", "--porcelain" ).strip.empty?
|
|
743
746
|
end
|
|
744
747
|
|
|
748
|
+
def push_prep_commit!
|
|
749
|
+
# JIT auto-commit is for feature branches only; main is protected from direct commits.
|
|
750
|
+
return if current_branch == config.main_branch
|
|
751
|
+
|
|
752
|
+
dirty = managed_dirty_paths
|
|
753
|
+
return if dirty.empty?
|
|
754
|
+
|
|
755
|
+
git_system!( "add", *dirty )
|
|
756
|
+
git_system!( "commit", "-m", "chore: sync Carson managed files" )
|
|
757
|
+
puts_line "Carson committed managed file updates."
|
|
758
|
+
end
|
|
759
|
+
|
|
760
|
+
def managed_dirty_paths
|
|
761
|
+
template_paths = config.template_managed_files + config.template_superseded_files
|
|
762
|
+
linters_glob = Dir.glob( File.join( repo_root, ".github/linters/**/*" ) )
|
|
763
|
+
.select { |p| File.file?( p ) }
|
|
764
|
+
.map { |p| p.delete_prefix( "#{repo_root}/" ) }
|
|
765
|
+
candidates = ( template_paths + linters_glob ).uniq
|
|
766
|
+
return [] if candidates.empty?
|
|
767
|
+
|
|
768
|
+
stdout_text, = git_capture_soft( "status", "--porcelain", "--", *candidates )
|
|
769
|
+
stdout_text.to_s.lines
|
|
770
|
+
.reject { |l| l.start_with?( "??" ) }
|
|
771
|
+
.map { |l| l[ 3.. ].strip }
|
|
772
|
+
.reject( &:empty? )
|
|
773
|
+
end
|
|
774
|
+
|
|
745
775
|
def inside_git_work_tree?
|
|
746
776
|
stdout_text, = git_capture_soft( "rev-parse", "--is-inside-work-tree" )
|
|
747
777
|
stdout_text.to_s.strip == "true"
|