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 +4 -4
- data/RELEASE.md +17 -0
- data/VERSION +1 -1
- data/lib/carson/cli.rb +7 -4
- data/lib/carson/runtime/deliver.rb +94 -29
- 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: 91c767b37c33c1d69a377f509b5503ef342fef4ff4f3629439e998938d99b24b
|
|
4
|
+
data.tar.gz: 2b615ad6df7610917d400166a5d735c9bc3f7e51b52eeda33d48446be9916d9c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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
|
-
|
|
19
|
-
|
|
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
|
-
|
|
24
|
-
return
|
|
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
|
-
|
|
33
|
+
if pr_number.nil?
|
|
34
|
+
return deliver_finish( result: result, exit_code: EXIT_ERROR, json_output: json_output )
|
|
35
|
+
end
|
|
31
36
|
|
|
32
|
-
|
|
37
|
+
result[ :pr_number ] = pr_number
|
|
38
|
+
result[ :pr_url ] = pr_url
|
|
33
39
|
|
|
34
40
|
# Without --merge, we are done.
|
|
35
|
-
|
|
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
|
-
|
|
51
|
+
# Continue to merge.
|
|
42
52
|
when :pending
|
|
43
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
55
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|