carson 3.2.0 → 3.3.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: 21eb861872b515965379fc3dc8afe24fceb228589b479e475999116aca5eca2a
4
- data.tar.gz: da1ac628ac9da29969d973a5265c7ddff472370c1865e08af69f8f7bff3203e5
3
+ metadata.gz: 91c767b37c33c1d69a377f509b5503ef342fef4ff4f3629439e998938d99b24b
4
+ data.tar.gz: 2b615ad6df7610917d400166a5d735c9bc3f7e51b52eeda33d48446be9916d9c
5
5
  SHA512:
6
- metadata.gz: ebafee0b573830243f4706fd5884ab9cece7ec07ecf657b936ea76d48c79af9eec30a9af2ac23ea1d7e7fbad604d49573aa67c2c53e9a500f16225684e81c67c
7
- data.tar.gz: 0aea5423f60e9d33ca4f42f15b5f7bc4ecfd219960cec6d65d0c868fdc53a44831cb0eb4d12264f489b1fd7baafbf55b7226386b9fb1a7e8e4f360b06f9106f7
6
+ metadata.gz: df671d4ed658b1aa98290cfcdbc5cd47c78c702d83254bdc4e79d9ed5755320b3cd9c08f664e20633830661506fe8a0cdcb0f8c03e1314623a6074a444f7d23d
7
+ data.tar.gz: e05a41a7fb961deac1f63eccb77b8d03eb7d694a557baebaead88f5f38d9bab91a1b4643f71b4bf09741e51946ab2c6707a288e7a22fe2ea981ee5769e370b2f
data/RELEASE.md CHANGED
@@ -5,6 +5,23 @@ Release-note scope rule:
5
5
  - `RELEASE.md` records only version deltas, breaking changes, and migration actions.
6
6
  - Operational usage guides live in `MANUAL.md` and `API.md`.
7
7
 
8
+ ## 3.3.0 — Deliver Refinements
9
+
10
+ ### What changed
11
+
12
+ - **`carson deliver --json`** — machine-readable JSON output for agent consumption. The JSON envelope includes `command`, `branch`, `pr_number`, `pr_url`, `ci`, `merged`, `exit_code`, and — on failure — `error` and `recovery` fields.
13
+ - **Recovery-aware errors** — every deliver error path now includes a concrete `recovery` command showing the user exactly what to run next. Human output shows `Recovery: <command>`, JSON output includes a `recovery` field.
14
+
15
+ ### UX
16
+
17
+ - JSON output uses `JSON.pretty_generate` for readability when inspected by humans.
18
+ - Recovery commands are context-specific: push failures suggest `git pull --rebase && git push`, main-branch errors suggest `git checkout -b <branch>`, CI failures suggest `gh pr checks` with re-deliver.
19
+ - Human output for CI pending and CI fail states now includes recovery guidance.
20
+
21
+ ### Migration
22
+
23
+ - No breaking changes. `carson deliver` without `--json` behaves identically to 3.2.0.
24
+
8
25
  ## 3.2.0 — Deliver
9
26
 
10
27
  ### What changed
data/VERSION CHANGED
@@ -1 +1 @@
1
- 3.2.0
1
+ 3.3.0
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 [status [--json]|setup|audit|sync|deliver [--merge] [--title T] [--body-file F]|prune [--all]|worktree create|done|remove <name>|onboard|refresh [--all]|offboard|template check|apply|review gate|sweep|govern [--dry-run] [--json] [--loop SECONDS]|version]"
56
+ opts.banner = "Usage: carson [status [--json]|setup|audit|sync|deliver [--merge] [--json] [--title T] [--body-file F]|prune [--all]|worktree create|done|remove <name>|onboard|refresh [--all]|offboard|template check|apply|review gate|sweep|govern [--dry-run] [--json] [--loop SECONDS]|version]"
57
57
  end
58
58
  end
59
59
 
@@ -252,10 +252,11 @@ module Carson
252
252
  end
253
253
 
254
254
  def self.parse_deliver_command( argv:, err: )
255
- options = { merge: false, title: nil, body_file: nil }
255
+ options = { merge: false, json: false, title: nil, body_file: nil }
256
256
  deliver_parser = OptionParser.new do |opts|
257
- opts.banner = "Usage: carson deliver [--merge] [--title TITLE] [--body-file PATH]"
257
+ opts.banner = "Usage: carson deliver [--merge] [--json] [--title TITLE] [--body-file PATH]"
258
258
  opts.on( "--merge", "Also merge the PR if CI passes" ) { options[ :merge ] = true }
259
+ opts.on( "--json", "Machine-readable JSON output" ) { options[ :json ] = true }
259
260
  opts.on( "--title TITLE", "PR title (defaults to branch name)" ) { |v| options[ :title ] = v }
260
261
  opts.on( "--body-file PATH", "File containing PR body text" ) { |v| options[ :body_file ] = v }
261
262
  end
@@ -268,6 +269,7 @@ module Carson
268
269
  {
269
270
  command: "deliver",
270
271
  merge: options.fetch( :merge ),
272
+ json: options.fetch( :json ),
271
273
  title: options[ :title ],
272
274
  body_file: options[ :body_file ]
273
275
  }
@@ -348,7 +350,8 @@ module Carson
348
350
  runtime.deliver!(
349
351
  merge: parsed.fetch( :merge, false ),
350
352
  title: parsed.fetch( :title, nil ),
351
- body_file: parsed.fetch( :body_file, nil )
353
+ body_file: parsed.fetch( :body_file, nil ),
354
+ json_output: parsed.fetch( :json, false )
352
355
  )
353
356
  when "review:gate"
354
357
  runtime.review_gate!
@@ -2,73 +2,135 @@
2
2
  # Collapses the 8-step manual PR flow into one or two commands.
3
3
  # `carson deliver` pushes and creates the PR.
4
4
  # `carson deliver --merge` also merges if CI is green.
5
+ # `carson deliver --json` outputs structured result for agent consumption.
5
6
  module Carson
6
7
  class Runtime
7
8
  module Deliver
8
9
  # Entry point for `carson deliver`.
9
10
  # Pushes current branch, creates a PR if needed, reports the PR URL.
10
11
  # With merge: true, also merges if CI passes and cleans up.
11
- def deliver!( merge: false, title: nil, body_file: nil )
12
+ def deliver!( merge: false, title: nil, body_file: nil, json_output: false )
12
13
  branch = current_branch
13
14
  main = config.main_branch
14
15
  remote = config.git_remote
16
+ result = { command: "deliver", branch: branch }
15
17
 
16
18
  # Guard: cannot deliver from main.
17
19
  if branch == main
18
- puts_line "ERROR: cannot deliver from #{main}. Switch to a feature branch first."
19
- return EXIT_ERROR
20
+ result[ :error ] = "cannot deliver from #{main}"
21
+ result[ :recovery ] = "git checkout -b <branch-name>"
22
+ return deliver_finish( result: result, exit_code: EXIT_ERROR, json_output: json_output )
20
23
  end
21
24
 
22
25
  # Step 1: push the branch.
23
- push_result = push_branch!( branch: branch, remote: remote )
24
- return push_result unless push_result == EXIT_OK
26
+ push_exit = push_branch!( branch: branch, remote: remote, result: result )
27
+ return deliver_finish( result: result, exit_code: push_exit, json_output: json_output ) unless push_exit == EXIT_OK
25
28
 
26
29
  # Step 2: find or create the PR.
27
30
  pr_number, pr_url = find_or_create_pr!(
28
- branch: branch, title: title, body_file: body_file
31
+ branch: branch, title: title, body_file: body_file, result: result
29
32
  )
30
- return EXIT_ERROR if pr_number.nil?
33
+ if pr_number.nil?
34
+ return deliver_finish( result: result, exit_code: EXIT_ERROR, json_output: json_output )
35
+ end
31
36
 
32
- puts_line "PR: ##{pr_number} #{pr_url}"
37
+ result[ :pr_number ] = pr_number
38
+ result[ :pr_url ] = pr_url
33
39
 
34
40
  # Without --merge, we are done.
35
- return EXIT_OK unless merge
41
+ unless merge
42
+ return deliver_finish( result: result, exit_code: EXIT_OK, json_output: json_output )
43
+ end
36
44
 
37
45
  # Step 3: check CI status.
38
46
  ci_status = check_pr_ci( number: pr_number )
47
+ result[ :ci ] = ci_status.to_s
48
+
39
49
  case ci_status
40
50
  when :pass
41
- puts_line "CI: pass"
51
+ # Continue to merge.
42
52
  when :pending
43
- puts_line "CI: pending merge when checks complete."
44
- return EXIT_OK
53
+ result[ :recovery ] = "gh pr checks #{pr_number} --watch && carson deliver --merge"
54
+ return deliver_finish( result: result, exit_code: EXIT_OK, json_output: json_output )
45
55
  when :fail
46
- puts_line "CI: failing — fix before merging."
47
- return EXIT_BLOCK
56
+ result[ :recovery ] = "gh pr checks #{pr_number} — fix failures, push, then `carson deliver --merge`"
57
+ return deliver_finish( result: result, exit_code: EXIT_BLOCK, json_output: json_output )
48
58
  else
49
- puts_line "CI: unknown check manually."
50
- return EXIT_OK
59
+ result[ :recovery ] = "gh pr checks #{pr_number}"
60
+ return deliver_finish( result: result, exit_code: EXIT_OK, json_output: json_output )
51
61
  end
52
62
 
53
63
  # Step 4: merge.
54
- merge_result = merge_pr!( number: pr_number )
55
- return merge_result unless merge_result == EXIT_OK
64
+ merge_exit = merge_pr!( number: pr_number, result: result )
65
+ return deliver_finish( result: result, exit_code: merge_exit, json_output: json_output ) unless merge_exit == EXIT_OK
66
+
67
+ result[ :merged ] = true
56
68
 
57
69
  # Step 5: sync main.
58
70
  sync_after_merge!( remote: remote, main: main )
59
71
 
60
- EXIT_OK
72
+ deliver_finish( result: result, exit_code: EXIT_OK, json_output: json_output )
61
73
  end
62
74
 
63
75
  private
64
76
 
77
+ # Outputs the final result — JSON or human-readable — and returns exit code.
78
+ def deliver_finish( result:, exit_code:, json_output: )
79
+ result[ :exit_code ] = exit_code
80
+
81
+ if json_output
82
+ out.puts JSON.pretty_generate( result )
83
+ else
84
+ print_deliver_human( result: result )
85
+ end
86
+
87
+ exit_code
88
+ end
89
+
90
+ # Human-readable output for deliver results.
91
+ def print_deliver_human( result: )
92
+ exit_code = result.fetch( :exit_code )
93
+
94
+ if result[ :error ]
95
+ puts_line "ERROR: #{result[ :error ]}"
96
+ puts_line " Recovery: #{result[ :recovery ]}" if result[ :recovery ]
97
+ return
98
+ end
99
+
100
+ if result[ :pr_number ]
101
+ puts_line "PR: ##{result[ :pr_number ]} #{result[ :pr_url ]}"
102
+ end
103
+
104
+ if result[ :ci ]
105
+ ci = result[ :ci ]
106
+ case ci
107
+ when "pass"
108
+ puts_line "CI: pass"
109
+ when "pending"
110
+ puts_line "CI: pending — merge when checks complete."
111
+ puts_line " Recovery: #{result[ :recovery ]}" if result[ :recovery ]
112
+ when "fail"
113
+ puts_line "CI: failing — fix before merging."
114
+ puts_line " Recovery: #{result[ :recovery ]}" if result[ :recovery ]
115
+ else
116
+ puts_line "CI: #{ci} — check manually."
117
+ puts_line " Recovery: #{result[ :recovery ]}" if result[ :recovery ]
118
+ end
119
+ end
120
+
121
+ if result[ :merged ]
122
+ puts_line "Merged PR ##{result[ :pr_number ]} via #{result[ :merge_method ]}."
123
+ end
124
+ end
125
+
65
126
  # Pushes the branch to the remote with tracking.
66
- def push_branch!( branch:, remote: )
127
+ def push_branch!( branch:, remote:, result: )
67
128
  _, push_stderr, push_success, = git_run( "push", "-u", remote, branch )
68
129
  unless push_success
69
130
  error_text = push_stderr.to_s.strip
70
131
  error_text = "push failed" if error_text.empty?
71
- puts_line "ERROR: #{error_text}"
132
+ result[ :error ] = error_text
133
+ result[ :recovery ] = "git pull #{remote} #{branch} --rebase && git push -u #{remote} #{branch}"
72
134
  return EXIT_ERROR
73
135
  end
74
136
  puts_verbose "pushed #{branch} to #{remote}"
@@ -77,13 +139,13 @@ module Carson
77
139
 
78
140
  # Finds an existing PR for the branch, or creates a new one.
79
141
  # Returns [number, url] or [nil, nil] on failure.
80
- def find_or_create_pr!( branch:, title: nil, body_file: nil )
142
+ def find_or_create_pr!( branch:, title: nil, body_file: nil, result: )
81
143
  # Check for existing PR.
82
144
  existing = find_existing_pr( branch: branch )
83
145
  return existing if existing.first
84
146
 
85
147
  # Create a new PR.
86
- create_pr!( branch: branch, title: title, body_file: body_file )
148
+ create_pr!( branch: branch, title: title, body_file: body_file, result: result )
87
149
  end
88
150
 
89
151
  # Queries gh for an open PR on this branch.
@@ -104,7 +166,7 @@ module Carson
104
166
 
105
167
  # Creates a PR via gh. Title defaults to branch name humanised.
106
168
  # Returns [number, url] or [nil, nil] on failure.
107
- def create_pr!( branch:, title: nil, body_file: nil )
169
+ def create_pr!( branch:, title: nil, body_file: nil, result: )
108
170
  pr_title = title || default_pr_title( branch: branch )
109
171
 
110
172
  args = [ "pr", "create", "--title", pr_title, "--head", branch ]
@@ -118,7 +180,8 @@ module Carson
118
180
  unless success
119
181
  error_text = stderr.to_s.strip
120
182
  error_text = "pr create failed" if error_text.empty?
121
- puts_line "ERROR: #{error_text}"
183
+ result[ :error ] = error_text
184
+ result[ :recovery ] = "gh pr create --title '#{pr_title}' --head #{branch}"
122
185
  return [ nil, nil ]
123
186
  end
124
187
 
@@ -160,21 +223,23 @@ module Carson
160
223
  end
161
224
 
162
225
  # Merges the PR using the configured merge method.
163
- def merge_pr!( number: )
226
+ def merge_pr!( number:, result: )
164
227
  method = config.govern_merge_method
165
- stdout, stderr, success, = gh_run(
228
+ result[ :merge_method ] = method
229
+
230
+ _, stderr, success, = gh_run(
166
231
  "pr", "merge", number.to_s,
167
232
  "--#{method}",
168
233
  "--delete-branch"
169
234
  )
170
235
 
171
236
  if success
172
- puts_line "Merged PR ##{number} via #{method}."
173
237
  EXIT_OK
174
238
  else
175
239
  error_text = stderr.to_s.strip
176
240
  error_text = "merge failed" if error_text.empty?
177
- puts_line "ERROR: #{error_text}"
241
+ result[ :error ] = error_text
242
+ result[ :recovery ] = "gh pr merge #{number} --#{method} --delete-branch"
178
243
  EXIT_ERROR
179
244
  end
180
245
  end
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: 3.2.0
4
+ version: 3.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hailei Wang