carson 2.14.1 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dad32a3bc4067fc3833f9ff468633f6167862dfbeadd229e085998a54887fba6
4
- data.tar.gz: e3653cbe7da475a2f8a9439493481a91fb2737978a1206a5caa964019d3d9e80
3
+ metadata.gz: e14a15b6d3d91c0bbf75d0b3652a65ee7339b2c2cdeb56cd0937efe94f90c584
4
+ data.tar.gz: 1b08caaaf7a4e453ce55256eca2c961b09ffaaae761a6be101e10e3ab0f61397
5
5
  SHA512:
6
- metadata.gz: b1b3d0488e5410c1319c16c3e5c90577fc9d63307546c2a5c530f6d4af4898ce80f8b17978a7e50d17a6c16c17fdf3acedb35f27920127ef0c24086c0ca9b300
7
- data.tar.gz: 3c37e91e582c39553732ed7f9ec002f72619f3b36213c9e77e87234636d68b62c141fd0b778c2e1b48e2f3d35555193cbfcd7d58b81761d2181113213761d08f
6
+ metadata.gz: d594bcef84c8aa142438ef8b625c71f38366bb19e4a7a8abc48b415b882946b93845d226cac1eecc08a316b94677b8bcdf02edcdb9eac70a55f1f85fd800922b
7
+ data.tar.gz: 02ea0cc8507330c1dbb8e7e1b6a1f8e4f3a88f0d9aa396b296251cbd70fe916f4fcdb7b564301427d6911052376071f35098e34021c19cb6c28e7d05c84a0f79
data/VERSION CHANGED
@@ -1 +1 @@
1
- 2.14.1
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
- parse_named_subcommand( command: command, usage: "check|apply", argv: argv, parser: parser, err: err )
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 ),
@@ -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 = {
@@ -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? ? EXIT_ERROR : EXIT_OK
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"
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.14.1
4
+ version: 2.15.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hailei Wang