carson 2.14.2 → 2.15.1
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/API.md +3 -2
- data/RELEASE.md +40 -0
- data/VERSION +1 -1
- data/hooks/pre-push +10 -0
- data/lib/carson/cli.rb +34 -3
- data/lib/carson/runtime/audit.rb +61 -1
- 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: d3142430a752f707dc361e222db4581ad007baf32da8e041fc2f4c83475fe26a
|
|
4
|
+
data.tar.gz: aaefa0ac48072fa8ef9427b79617dac2f82492828b596bb8f9755c2c71c093f7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8ccd8f2cf691b6d44bd6435d49db141cf01ee4dbf93f1c8b7c37b63ac82ce923ef72c10455118628d2431fa312a344530822bc2d5e4aaecdc89b3487ec34e26e
|
|
7
|
+
data.tar.gz: a24f069a42dfb80d938aef2e1cb2e397180d59b52b658d80cb47824d26151b06d62dc6904b95e004d3a9440bacb0c9f76a162cea78d406335ae9e5b874bca4e1
|
data/API.md
CHANGED
|
@@ -56,6 +56,7 @@ carson <command> [subcommand] [arguments]
|
|
|
56
56
|
|---|---|
|
|
57
57
|
| `carson version` | Print installed Carson version. |
|
|
58
58
|
| `carson inspect` | Verify Carson-managed hook installation and repository setup. |
|
|
59
|
+
| `carson check` | Report required CI check status for the current branch's open PR. Exits 0 for passing or pending; exits 2 for failing. Never exits 8. |
|
|
59
60
|
|
|
60
61
|
## Exit status contract
|
|
61
62
|
|
|
@@ -118,7 +119,7 @@ Environment overrides:
|
|
|
118
119
|
},
|
|
119
120
|
"check_wait": 30,
|
|
120
121
|
"merge": {
|
|
121
|
-
"authority":
|
|
122
|
+
"authority": true,
|
|
122
123
|
"method": "squash"
|
|
123
124
|
}
|
|
124
125
|
}
|
|
@@ -130,7 +131,7 @@ Environment overrides:
|
|
|
130
131
|
- `agent.provider`: `"auto"`, `"codex"`, or `"claude"`.
|
|
131
132
|
- `agent.codex` / `agent.claude`: provider-specific options (reserved).
|
|
132
133
|
- `check_wait`: seconds to wait for CI checks before classifying (default: `30`).
|
|
133
|
-
- `merge.authority`: `
|
|
134
|
+
- `merge.authority`: `true` (default) — Carson may merge autonomously. Set to `false` to require explicit enablement.
|
|
134
135
|
- `merge.method`: `"squash"` (default), `"merge"`, or `"rebase"`.
|
|
135
136
|
|
|
136
137
|
`lint` schema:
|
data/RELEASE.md
CHANGED
|
@@ -5,6 +5,46 @@ 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
|
+
## 2.15.1 — Codex Review Fixes
|
|
9
|
+
|
|
10
|
+
### What changed
|
|
11
|
+
|
|
12
|
+
- `carson check` now correctly exits 2 for cancelled, errored, or timed-out CI checks. Previously, only `fail`-bucketed checks were treated as failing — all other non-passing states (cancelled, error) fell through to "all passing".
|
|
13
|
+
- Pre-push auto-commit now aborts the in-flight push and prints "Push again to include them." Previously the commit was created locally but not included in the push that triggered it.
|
|
14
|
+
- `--push-prep` now stages and commits untracked managed files. Previously, new managed files introduced by a gem upgrade were silently omitted.
|
|
15
|
+
- `API.md`: `merge.authority` default corrected from `false` to `true` to match the implementation.
|
|
16
|
+
- `API.md`: `carson check` added to the Info commands table.
|
|
17
|
+
- `docs/develop.md`: Architecture rationale updated — `govern.rb` acknowledged as a known exception to the adapter shell-out rule.
|
|
18
|
+
- Test coverage added for `check` bucket classification, `managed_dirty_paths` untracked handling, and CLI dispatch.
|
|
19
|
+
|
|
20
|
+
### No migration required
|
|
21
|
+
|
|
22
|
+
No configuration or workflow changes needed.
|
|
23
|
+
|
|
24
|
+
## 2.15.0 — JIT Auto-Commit on Pre-Push + `carson check`
|
|
25
|
+
|
|
26
|
+
### What changed
|
|
27
|
+
|
|
28
|
+
- Pre-push hook now calls `carson template apply --push-prep` automatically. Any uncommitted changes to Carson-managed template files or `.github/linters/` are committed before the push reaches GitHub — no manual `git add` / `git commit` needed after a gem upgrade or `lint policy` run.
|
|
29
|
+
- `--push-prep` flag scopes the behaviour to pre-push only; interactive `carson template apply` is unchanged.
|
|
30
|
+
- `carson check` command added: wraps `gh pr checks --required`, exits 0 for pending or passing and 2 for failing. Useful for callers that need a clean CI status signal without `gh`'s confusing "Error: Exit code 8" for pending runs.
|
|
31
|
+
- CI smoke tests guarded against live audit exit codes with `|| true` so a pending or failing CI run on the default branch does not cause false test failures.
|
|
32
|
+
|
|
33
|
+
### No migration required
|
|
34
|
+
|
|
35
|
+
Run `bash install.sh` to pick up the updated pre-push hook in all governed repos.
|
|
36
|
+
|
|
37
|
+
## 2.14.2 — Docs Enrichment
|
|
38
|
+
|
|
39
|
+
### What changed
|
|
40
|
+
|
|
41
|
+
- `docs/design.md` enriched with signal system, output design, prompt principles, and vocabulary guide.
|
|
42
|
+
- `docs/develop.md` enriched with architecture rationale, new-command walkthrough, and testing approach.
|
|
43
|
+
|
|
44
|
+
### No migration required
|
|
45
|
+
|
|
46
|
+
Documentation only. No behavioural changes.
|
|
47
|
+
|
|
8
48
|
## 2.14.1 — Auto-Refresh on Install
|
|
9
49
|
|
|
10
50
|
### What changed
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
2.
|
|
1
|
+
2.15.1
|
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
|
+
pending = checks_data.select { |e| e[ "bucket" ].to_s == "pending" }
|
|
108
|
+
failing = checks_data.select { |e| check_entry_failing?( entry: e ) }
|
|
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 = {
|
|
@@ -127,8 +181,8 @@ module Carson
|
|
|
127
181
|
return report
|
|
128
182
|
end
|
|
129
183
|
checks_data = JSON.parse( checks_stdout )
|
|
130
|
-
failing = checks_data.select { |entry| entry[ "bucket" ].to_s == "fail" || entry[ "state" ].to_s.upcase == "FAILURE" }
|
|
131
184
|
pending = checks_data.select { |entry| entry[ "bucket" ].to_s == "pending" }
|
|
185
|
+
failing = checks_data.select { |entry| check_entry_failing?( entry: entry ) }
|
|
132
186
|
report[ :checks ][ :status ] = checks_success ? "ok" : ( checks_exit == 8 ? "pending" : "attention" )
|
|
133
187
|
report[ :checks ][ :required_total ] = checks_data.count
|
|
134
188
|
report[ :checks ][ :failing_count ] = failing.count
|
|
@@ -307,6 +361,12 @@ module Carson
|
|
|
307
361
|
[ critical, advisory ]
|
|
308
362
|
end
|
|
309
363
|
|
|
364
|
+
# Returns true when a required-check entry is in a non-passing, non-pending state.
|
|
365
|
+
# Cancelled, errored, timed-out, and any unknown bucket all count as failing.
|
|
366
|
+
def check_entry_failing?( entry: )
|
|
367
|
+
!%w[pass pending].include?( entry[ "bucket" ].to_s )
|
|
368
|
+
end
|
|
369
|
+
|
|
310
370
|
# Failing means completed with a non-successful conclusion.
|
|
311
371
|
def default_branch_check_run_failing?( entry: )
|
|
312
372
|
status = entry[ "status" ].to_s.strip.downcase
|
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
|
+
return EXIT_BLOCK if push_prep && push_prep_commit!
|
|
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. Push again to include them."
|
|
758
|
+
true
|
|
759
|
+
end
|
|
760
|
+
|
|
761
|
+
def managed_dirty_paths
|
|
762
|
+
template_paths = config.template_managed_files + config.template_superseded_files
|
|
763
|
+
linters_glob = Dir.glob( File.join( repo_root, ".github/linters/**/*" ) )
|
|
764
|
+
.select { |p| File.file?( p ) }
|
|
765
|
+
.map { |p| p.delete_prefix( "#{repo_root}/" ) }
|
|
766
|
+
candidates = ( template_paths + linters_glob ).uniq
|
|
767
|
+
return [] if candidates.empty?
|
|
768
|
+
|
|
769
|
+
stdout_text, = git_capture_soft( "status", "--porcelain", "--", *candidates )
|
|
770
|
+
stdout_text.to_s.lines
|
|
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"
|