carson 2.8.1 → 2.10.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/API.md +1 -0
- data/MANUAL.md +1 -0
- data/RELEASE.md +45 -0
- data/VERSION +1 -1
- data/carson.gemspec +1 -1
- data/hooks/pre-merge-commit +1 -1
- data/hooks/pre-push +1 -1
- data/hooks/prepare-commit-msg +1 -1
- data/lib/carson/cli.rb +6 -3
- data/lib/carson/config.rb +1 -1
- data/lib/carson/policy/ruby/lint.rb +0 -1
- data/lib/carson/runtime/audit.rb +121 -95
- data/lib/carson/runtime/govern.rb +9 -4
- data/lib/carson/runtime/lint.rb +9 -8
- data/lib/carson/runtime/local.rb +110 -55
- data/lib/carson/runtime/review/gate_support.rb +6 -6
- data/lib/carson/runtime/review/sweep_support.rb +3 -3
- data/lib/carson/runtime/review.rb +17 -9
- data/lib/carson/runtime/setup.rb +9 -8
- data/lib/carson/runtime.rb +10 -5
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9b99975ce01b36b30e333553f7c491ebbf92591a3bc7dbc492633984b80a807a
|
|
4
|
+
data.tar.gz: 55c64697792a9400be1fef45a68731ac7eab6be3be517c79ae2caebd5f5edc9c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c716497d2db2c235dc5d8fa90c65fda8002722cabd7e8425305ce9c26917ebd48e905aed20564393d01178bd8d12257606097fdbf3ab1a77be71d1e48dc08681
|
|
7
|
+
data.tar.gz: 7f5358b84901463fbd1b211c3094df9c7c46566f9decc3a99d4d643264fc76e79de8b3c298d642c6efac41ecc16aa07647b52e66b6a822f7fff842345dfd9ab6
|
data/API.md
CHANGED
data/MANUAL.md
CHANGED
|
@@ -212,6 +212,7 @@ Common environment overrides:
|
|
|
212
212
|
| `CARSON_REVIEW_DISPOSITION_PREFIX` | Required prefix for disposition comments. |
|
|
213
213
|
| `CARSON_REVIEW_SWEEP_WINDOW_DAYS` | Lookback window for review sweep. |
|
|
214
214
|
| `CARSON_REVIEW_SWEEP_STATES` | PR states to include in sweep. |
|
|
215
|
+
| `CARSON_WORKFLOW_STYLE` | Workflow style override (`branch` or `trunk`). |
|
|
215
216
|
| `CARSON_RUBY_INDENTATION` | Ruby indentation policy (`tabs`, `spaces`, or `either`). |
|
|
216
217
|
|
|
217
218
|
For the full configuration schema and `lint.languages` definition, see `API.md`.
|
data/RELEASE.md
CHANGED
|
@@ -5,6 +5,51 @@ 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.10.0 — Lower Ruby Requirement to 3.4
|
|
9
|
+
|
|
10
|
+
### What changed
|
|
11
|
+
|
|
12
|
+
- **Minimum Ruby version lowered from 4.0 to 3.4.** Carson uses no Ruby 4.0-specific features. Lowering to 3.4 widens compatibility to the current stable Ruby series while enabling the `it` implicit block parameter.
|
|
13
|
+
- **Removed `# frozen_string_literal: true` pragma** from the one file that had it (`lib/carson/policy/ruby/lint.rb`). Ruby 4.0 freezes strings by default; the pragma is unnecessary.
|
|
14
|
+
- **Default workflow style now actually `branch` in code.** The 2.9.0 release notes documented this change, but the hooks and config default were not updated. Now fixed: hooks fall back to `branch`, config default is `branch`, and `CARSON_WORKFLOW_STYLE` env override is documented.
|
|
15
|
+
|
|
16
|
+
### What users must do now
|
|
17
|
+
|
|
18
|
+
1. Upgrade Carson to `2.10.0`.
|
|
19
|
+
2. Ruby 3.4 or later is now sufficient — Ruby 4.0 is no longer required.
|
|
20
|
+
|
|
21
|
+
## 2.9.0 — Concise UX for All Commands
|
|
22
|
+
|
|
23
|
+
### What changed
|
|
24
|
+
|
|
25
|
+
- **Concise output by default.** Every Carson command now prints clean, minimal output — what happened, what needs attention, what to do next. Diagnostic key-value lines are suppressed unless `--verbose` is passed.
|
|
26
|
+
- **`--verbose` flag.** Global flag available on all commands. Restores full diagnostic output (same as pre-2.9.0 behaviour). The pre-commit hook runs `carson audit` (no flags) so it automatically gets clean output.
|
|
27
|
+
- **Audit concise output.** A healthy audit prints one line (`Audit: ok`). Problems print only actionable summaries (e.g. `Hooks: mismatch — run carson prepare.`).
|
|
28
|
+
- **Refresh concise output.** Prints ~5 lines: hooks installed, templates in sync, audit result, done.
|
|
29
|
+
- **All other commands.** `prepare`, `inspect`, `offboard`, `template check/apply`, `prune`, `review gate/sweep`, `govern`, `lint setup`, `setup`, and `housekeep` all follow the same concise/verbose pattern.
|
|
30
|
+
- **Default workflow style changed from `trunk` to `branch`.** All governed repositories now enforce PR-only merges by default. Direct commits, merge commits, and pushes to protected branches (`main`/`master`) are blocked by hooks unless explicitly opted out.
|
|
31
|
+
|
|
32
|
+
### What users must do now
|
|
33
|
+
|
|
34
|
+
1. Upgrade Carson to `2.9.0`.
|
|
35
|
+
2. Use `--verbose` when you need full diagnostics (debugging, CI troubleshooting).
|
|
36
|
+
3. If you rely on direct commits to main, re-run `carson setup` and choose `trunk`, or set `CARSON_WORKFLOW_STYLE=trunk` in your environment.
|
|
37
|
+
|
|
38
|
+
### Breaking or removed behaviour
|
|
39
|
+
|
|
40
|
+
- Default output is now concise. Scripts that parse Carson's key-value diagnostic lines must add `--verbose`.
|
|
41
|
+
- Removed `@concise` internal flag (replaced by `--verbose` opt-in pattern).
|
|
42
|
+
- Default `workflow.style` changed from `trunk` to `branch`. Repositories that previously relied on the implicit `trunk` default will now block direct commits to protected branches. Escape hatches: run `carson setup` to choose `trunk`, or set `CARSON_WORKFLOW_STYLE=trunk`.
|
|
43
|
+
|
|
44
|
+
### Upgrade steps
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
cd ~/Dev/carson
|
|
48
|
+
git pull
|
|
49
|
+
bash install.sh
|
|
50
|
+
carson version
|
|
51
|
+
```
|
|
52
|
+
|
|
8
53
|
## 2.8.1 — Onboard UX and Install Cleanup
|
|
9
54
|
|
|
10
55
|
### What changed
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
2.
|
|
1
|
+
2.10.0
|
data/carson.gemspec
CHANGED
|
@@ -11,7 +11,7 @@ Gem::Specification.new do |spec|
|
|
|
11
11
|
spec.description = "Carson runs outside host repositories and applies governance checks, review gates, and managed GitHub-native files."
|
|
12
12
|
spec.homepage = "https://github.com/wanghailei/carson"
|
|
13
13
|
spec.license = "MIT"
|
|
14
|
-
spec.required_ruby_version = ">= 4
|
|
14
|
+
spec.required_ruby_version = ">= 3.4"
|
|
15
15
|
spec.metadata = {
|
|
16
16
|
"source_code_uri" => "https://github.com/wanghailei/carson",
|
|
17
17
|
"changelog_uri" => "https://github.com/wanghailei/carson/blob/main/RELEASE.md",
|
data/hooks/pre-merge-commit
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
set -euo pipefail
|
|
3
3
|
|
|
4
4
|
hooks_dir="$(cd "$(dirname "$0")" && pwd)"
|
|
5
|
-
style="$(cat "$hooks_dir/workflow_style" 2>/dev/null || echo "
|
|
5
|
+
style="$(cat "$hooks_dir/workflow_style" 2>/dev/null || echo "branch")"
|
|
6
6
|
[ "$style" = "trunk" ] && exit 0
|
|
7
7
|
|
|
8
8
|
branch_name="$(git rev-parse --abbrev-ref HEAD)"
|
data/hooks/pre-push
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
set -euo pipefail
|
|
3
3
|
|
|
4
4
|
hooks_dir="$(cd "$(dirname "$0")" && pwd)"
|
|
5
|
-
style="$(cat "$hooks_dir/workflow_style" 2>/dev/null || echo "
|
|
5
|
+
style="$(cat "$hooks_dir/workflow_style" 2>/dev/null || echo "branch")"
|
|
6
6
|
[ "$style" = "trunk" ] && exit 0
|
|
7
7
|
|
|
8
8
|
remote_name="${1:-unknown}"
|
data/hooks/prepare-commit-msg
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
set -euo pipefail
|
|
3
3
|
|
|
4
4
|
hooks_dir="$(cd "$(dirname "$0")" && pwd)"
|
|
5
|
-
style="$(cat "$hooks_dir/workflow_style" 2>/dev/null || echo "
|
|
5
|
+
style="$(cat "$hooks_dir/workflow_style" 2>/dev/null || echo "branch")"
|
|
6
6
|
[ "$style" = "trunk" ] && exit 0
|
|
7
7
|
|
|
8
8
|
branch_name="$(git rev-parse --abbrev-ref HEAD)"
|
data/lib/carson/cli.rb
CHANGED
|
@@ -19,7 +19,8 @@ module Carson
|
|
|
19
19
|
return Runtime::EXIT_ERROR
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
verbose = parsed.fetch( :verbose, false )
|
|
23
|
+
runtime = Runtime.new( repo_root: target_repo_root, tool_root: tool_root, out: out, err: err, verbose: verbose )
|
|
23
24
|
dispatch( parsed: parsed, runtime: runtime )
|
|
24
25
|
rescue ConfigError => e
|
|
25
26
|
err.puts "#{BADGE} CONFIG ERROR: #{e.message}"
|
|
@@ -30,12 +31,14 @@ module Carson
|
|
|
30
31
|
end
|
|
31
32
|
|
|
32
33
|
def self.parse_args( argv:, out:, err: )
|
|
34
|
+
verbose = argv.delete( "--verbose" ) ? true : false
|
|
33
35
|
parser = build_parser
|
|
34
36
|
preset = parse_preset_command( argv: argv, out: out, parser: parser )
|
|
35
|
-
return preset unless preset.nil?
|
|
37
|
+
return preset.merge( verbose: verbose ) unless preset.nil?
|
|
36
38
|
|
|
37
39
|
command = argv.shift
|
|
38
|
-
parse_command( command: command, argv: argv, parser: parser, err: err )
|
|
40
|
+
result = parse_command( command: command, argv: argv, parser: parser, err: err )
|
|
41
|
+
result.merge( verbose: verbose )
|
|
39
42
|
rescue OptionParser::ParseError => e
|
|
40
43
|
err.puts "#{BADGE} #{e.message}"
|
|
41
44
|
err.puts parser
|
data/lib/carson/config.rb
CHANGED
data/lib/carson/runtime/audit.rb
CHANGED
|
@@ -8,42 +8,62 @@ module Carson
|
|
|
8
8
|
fingerprint_status = block_if_outsider_fingerprints!
|
|
9
9
|
return fingerprint_status unless fingerprint_status.nil?
|
|
10
10
|
audit_state = "ok"
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
audit_concise_problems = []
|
|
12
|
+
puts_verbose ""
|
|
13
|
+
puts_verbose "[Repository]"
|
|
14
|
+
puts_verbose "root: #{repo_root}"
|
|
15
|
+
puts_verbose "current_branch: #{current_branch}"
|
|
16
|
+
puts_verbose ""
|
|
17
|
+
puts_verbose "[Working Tree]"
|
|
18
|
+
puts_verbose git_capture!( "status", "--short", "--branch" ).strip
|
|
19
|
+
puts_verbose ""
|
|
20
|
+
puts_verbose "[Hooks]"
|
|
17
21
|
hooks_ok = hooks_health_report
|
|
18
|
-
|
|
19
|
-
|
|
22
|
+
unless hooks_ok
|
|
23
|
+
audit_state = "block"
|
|
24
|
+
audit_concise_problems << "Hooks: mismatch — run carson prepare."
|
|
25
|
+
end
|
|
26
|
+
puts_verbose ""
|
|
27
|
+
puts_verbose "[Local Lint Quality]"
|
|
20
28
|
local_lint_quality = local_lint_quality_report
|
|
21
|
-
|
|
22
|
-
|
|
29
|
+
if local_lint_quality.fetch( :status ) == "block"
|
|
30
|
+
audit_state = "block"
|
|
31
|
+
blocking_langs = local_lint_quality.fetch( :languages ).select { |l| l.fetch( :status ) == "block" }
|
|
32
|
+
blocking_langs.each do |lang|
|
|
33
|
+
exit_code = lang.fetch( :exit_code, 1 )
|
|
34
|
+
audit_concise_problems << "Lint: #{lang.fetch( :language )} failed (exit #{exit_code})."
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
puts_verbose ""
|
|
38
|
+
puts_verbose "[Main Sync Status]"
|
|
23
39
|
ahead_count, behind_count, main_error = main_sync_counts
|
|
24
40
|
if main_error
|
|
25
|
-
|
|
26
|
-
|
|
41
|
+
puts_verbose "main_vs_remote_main: unknown"
|
|
42
|
+
puts_verbose "WARN: unable to calculate main sync status (#{main_error})."
|
|
27
43
|
audit_state = "attention" if audit_state == "ok"
|
|
28
44
|
elsif ahead_count.positive?
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
45
|
+
puts_verbose "main_vs_remote_main_ahead: #{ahead_count}"
|
|
46
|
+
puts_verbose "main_vs_remote_main_behind: #{behind_count}"
|
|
47
|
+
puts_verbose "ACTION: local #{config.main_branch} is ahead of #{config.git_remote}/#{config.main_branch} by #{ahead_count} commit#{plural_suffix( count: ahead_count )}; reset local drift before commit/push workflows."
|
|
32
48
|
audit_state = "block"
|
|
49
|
+
audit_concise_problems << "Main sync: ahead by #{ahead_count} — reset local drift."
|
|
33
50
|
elsif behind_count.positive?
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
51
|
+
puts_verbose "main_vs_remote_main_ahead: #{ahead_count}"
|
|
52
|
+
puts_verbose "main_vs_remote_main_behind: #{behind_count}"
|
|
53
|
+
puts_verbose "ACTION: local #{config.main_branch} is behind #{config.git_remote}/#{config.main_branch} by #{behind_count} commit#{plural_suffix( count: behind_count )}; run carson sync."
|
|
37
54
|
audit_state = "attention" if audit_state == "ok"
|
|
55
|
+
audit_concise_problems << "Main sync: behind by #{behind_count} — run carson sync."
|
|
38
56
|
else
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
57
|
+
puts_verbose "main_vs_remote_main_ahead: 0"
|
|
58
|
+
puts_verbose "main_vs_remote_main_behind: 0"
|
|
59
|
+
puts_verbose "ACTION: local #{config.main_branch} is in sync with #{config.git_remote}/#{config.main_branch}."
|
|
42
60
|
end
|
|
43
|
-
|
|
61
|
+
puts_verbose ""
|
|
62
|
+
puts_verbose "[PR and Required Checks (gh)]"
|
|
44
63
|
monitor_report = pr_and_check_report
|
|
45
64
|
audit_state = "attention" if audit_state == "ok" && monitor_report.fetch( :status ) != "ok"
|
|
46
|
-
|
|
65
|
+
puts_verbose ""
|
|
66
|
+
puts_verbose "[Default Branch CI Baseline (gh)]"
|
|
47
67
|
default_branch_baseline = default_branch_ci_baseline_report
|
|
48
68
|
audit_state = "block" if default_branch_baseline.fetch( :status ) == "block"
|
|
49
69
|
audit_state = "attention" if audit_state == "ok" && default_branch_baseline.fetch( :status ) != "ok"
|
|
@@ -56,9 +76,14 @@ module Carson
|
|
|
56
76
|
audit_status: audit_state
|
|
57
77
|
)
|
|
58
78
|
)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
79
|
+
puts_verbose ""
|
|
80
|
+
puts_verbose "[Audit Result]"
|
|
81
|
+
puts_verbose "status: #{audit_state}"
|
|
82
|
+
puts_verbose( audit_state == "block" ? "ACTION: local policy block must be resolved before commit/push." : "ACTION: no local hard block detected." )
|
|
83
|
+
unless verbose?
|
|
84
|
+
audit_concise_problems.each { |problem| puts_line problem }
|
|
85
|
+
puts_line "Audit: #{audit_state}"
|
|
86
|
+
end
|
|
62
87
|
audit_state == "block" ? EXIT_BLOCK : EXIT_OK
|
|
63
88
|
end
|
|
64
89
|
|
|
@@ -83,7 +108,7 @@ module Carson
|
|
|
83
108
|
unless gh_available?
|
|
84
109
|
report[ :status ] = "skipped"
|
|
85
110
|
report[ :skip_reason ] = "gh CLI not available in PATH"
|
|
86
|
-
|
|
111
|
+
puts_verbose "SKIP: #{report.fetch( :skip_reason )}"
|
|
87
112
|
return report
|
|
88
113
|
end
|
|
89
114
|
pr_stdout, pr_stderr, pr_success, = gh_run( "pr", "view", current_branch, "--json", "number,title,url,state,reviewDecision" )
|
|
@@ -91,7 +116,7 @@ module Carson
|
|
|
91
116
|
error_text = gh_error_text( stdout_text: pr_stdout, stderr_text: pr_stderr, fallback: "unable to read PR for branch #{current_branch}" )
|
|
92
117
|
report[ :status ] = "skipped"
|
|
93
118
|
report[ :skip_reason ] = error_text
|
|
94
|
-
|
|
119
|
+
puts_verbose "SKIP: #{error_text}"
|
|
95
120
|
return report
|
|
96
121
|
end
|
|
97
122
|
pr_data = JSON.parse( pr_stdout )
|
|
@@ -102,16 +127,16 @@ module Carson
|
|
|
102
127
|
state: pr_data[ "state" ].to_s,
|
|
103
128
|
review_decision: blank_to( value: pr_data[ "reviewDecision" ], default: "NONE" )
|
|
104
129
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
130
|
+
puts_verbose "pr: ##{report.dig( :pr, :number )} #{report.dig( :pr, :title )}"
|
|
131
|
+
puts_verbose "url: #{report.dig( :pr, :url )}"
|
|
132
|
+
puts_verbose "review_decision: #{report.dig( :pr, :review_decision )}"
|
|
108
133
|
checks_stdout, checks_stderr, checks_success, checks_exit = gh_run( "pr", "checks", report.dig( :pr, :number ).to_s, "--required", "--json", "name,state,bucket,workflow,link" )
|
|
109
134
|
if checks_stdout.to_s.strip.empty?
|
|
110
135
|
error_text = gh_error_text( stdout_text: checks_stdout, stderr_text: checks_stderr, fallback: "required checks unavailable" )
|
|
111
136
|
report[ :checks ][ :status ] = "skipped"
|
|
112
137
|
report[ :checks ][ :skip_reason ] = error_text
|
|
113
138
|
report[ :status ] = "attention"
|
|
114
|
-
|
|
139
|
+
puts_verbose "checks: SKIP (#{error_text})"
|
|
115
140
|
return report
|
|
116
141
|
end
|
|
117
142
|
checks_data = JSON.parse( checks_stdout )
|
|
@@ -123,17 +148,17 @@ module Carson
|
|
|
123
148
|
report[ :checks ][ :pending_count ] = pending.count
|
|
124
149
|
report[ :checks ][ :failing ] = normalise_check_entries( entries: failing )
|
|
125
150
|
report[ :checks ][ :pending ] = normalise_check_entries( entries: pending )
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
report.dig( :checks, :failing ).each { |entry|
|
|
130
|
-
report.dig( :checks, :pending ).each { |entry|
|
|
151
|
+
puts_verbose "required_checks_total: #{report.dig( :checks, :required_total )}"
|
|
152
|
+
puts_verbose "required_checks_failing: #{report.dig( :checks, :failing_count )}"
|
|
153
|
+
puts_verbose "required_checks_pending: #{report.dig( :checks, :pending_count )}"
|
|
154
|
+
report.dig( :checks, :failing ).each { |entry| puts_verbose "check_fail: #{entry.fetch( :workflow )} / #{entry.fetch( :name )} #{entry.fetch( :link )}".strip }
|
|
155
|
+
report.dig( :checks, :pending ).each { |entry| puts_verbose "check_pending: #{entry.fetch( :workflow )} / #{entry.fetch( :name )} #{entry.fetch( :link )}".strip }
|
|
131
156
|
report[ :status ] = "attention" if report.dig( :checks, :failing_count ).positive? || report.dig( :checks, :pending_count ).positive?
|
|
132
157
|
report
|
|
133
158
|
rescue JSON::ParserError => e
|
|
134
159
|
report[ :status ] = "skipped"
|
|
135
160
|
report[ :skip_reason ] = "invalid gh JSON response (#{e.message})"
|
|
136
|
-
|
|
161
|
+
puts_verbose "SKIP: #{report.fetch( :skip_reason )}"
|
|
137
162
|
report
|
|
138
163
|
end
|
|
139
164
|
|
|
@@ -148,8 +173,8 @@ module Carson
|
|
|
148
173
|
blocking_languages: 0,
|
|
149
174
|
languages: []
|
|
150
175
|
}
|
|
151
|
-
|
|
152
|
-
|
|
176
|
+
puts_verbose "lint_target_source: #{target_source}"
|
|
177
|
+
puts_verbose "lint_target_files_total: #{target_files.count}"
|
|
153
178
|
config.lint_languages.each do |language, entry|
|
|
154
179
|
language_report = lint_language_report(
|
|
155
180
|
language: language,
|
|
@@ -162,7 +187,7 @@ module Carson
|
|
|
162
187
|
report[ :status ] = "block"
|
|
163
188
|
report[ :blocking_languages ] += 1
|
|
164
189
|
end
|
|
165
|
-
|
|
190
|
+
puts_verbose "lint_blocking_languages: #{report.fetch( :blocking_languages )}"
|
|
166
191
|
report
|
|
167
192
|
rescue StandardError => e
|
|
168
193
|
report ||= {
|
|
@@ -191,7 +216,7 @@ module Carson
|
|
|
191
216
|
if github_pull_request_event?
|
|
192
217
|
files = lint_target_files_for_pull_request
|
|
193
218
|
return [ files, "github_pull_request" ] unless files.nil?
|
|
194
|
-
|
|
219
|
+
puts_verbose "WARN: unable to resolve pull request changed files; falling back to full repository files."
|
|
195
220
|
end
|
|
196
221
|
|
|
197
222
|
if github_actions_environment?
|
|
@@ -265,16 +290,16 @@ module Carson
|
|
|
265
290
|
config_files: entry.fetch( :config_files ),
|
|
266
291
|
exit_code: 0
|
|
267
292
|
}
|
|
268
|
-
|
|
293
|
+
puts_verbose "lint_language: #{language} enabled=#{report.fetch( :enabled )} files=#{report.fetch( :file_count )}"
|
|
269
294
|
if language == "ruby" && outsider_mode?
|
|
270
295
|
local_rubocop_path = File.join( repo_root, ".rubocop.yml" )
|
|
271
296
|
if File.file?( local_rubocop_path )
|
|
272
297
|
report[ :status ] = "block"
|
|
273
298
|
report[ :reason ] = "repo-local RuboCop config is forbidden: #{relative_path( local_rubocop_path )}; remove it and use ~/.carson/lint/rubocop.yml."
|
|
274
299
|
report[ :exit_code ] = EXIT_BLOCK
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
300
|
+
puts_verbose "lint_#{language}_status: block"
|
|
301
|
+
puts_verbose "lint_#{language}_reason: #{report.fetch( :reason )}"
|
|
302
|
+
puts_verbose "ACTION: remove .rubocop.yml from this repository and run carson lint setup --source <path-or-git-url>."
|
|
278
303
|
return report
|
|
279
304
|
end
|
|
280
305
|
end
|
|
@@ -286,9 +311,9 @@ module Carson
|
|
|
286
311
|
report[ :status ] = "block"
|
|
287
312
|
report[ :reason ] = "missing config files: #{missing_config_files.join( ', ' )}"
|
|
288
313
|
report[ :exit_code ] = EXIT_BLOCK
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
314
|
+
puts_verbose "lint_#{language}_status: block"
|
|
315
|
+
puts_verbose "lint_#{language}_reason: #{report.fetch( :reason )}"
|
|
316
|
+
puts_verbose "ACTION: run carson lint setup --source <path-or-git-url> to prepare ~/.carson/lint policy files."
|
|
292
317
|
return report
|
|
293
318
|
end
|
|
294
319
|
|
|
@@ -298,16 +323,16 @@ module Carson
|
|
|
298
323
|
report[ :status ] = "block"
|
|
299
324
|
report[ :reason ] = "missing lint command"
|
|
300
325
|
report[ :exit_code ] = EXIT_BLOCK
|
|
301
|
-
|
|
302
|
-
|
|
326
|
+
puts_verbose "lint_#{language}_status: block"
|
|
327
|
+
puts_verbose "lint_#{language}_reason: #{report.fetch( :reason )}"
|
|
303
328
|
return report
|
|
304
329
|
end
|
|
305
330
|
unless command_available_for_lint?( command_name: command_name )
|
|
306
331
|
report[ :status ] = "block"
|
|
307
332
|
report[ :reason ] = "command not available: #{command_name}"
|
|
308
333
|
report[ :exit_code ] = EXIT_BLOCK
|
|
309
|
-
|
|
310
|
-
|
|
334
|
+
puts_verbose "lint_#{language}_status: block"
|
|
335
|
+
puts_verbose "lint_#{language}_reason: #{report.fetch( :reason )}"
|
|
311
336
|
return report
|
|
312
337
|
end
|
|
313
338
|
|
|
@@ -322,9 +347,9 @@ module Carson
|
|
|
322
347
|
fallback: "lint command failed for #{language}"
|
|
323
348
|
)
|
|
324
349
|
end
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
350
|
+
puts_verbose "lint_#{language}_status: #{report.fetch( :status )}"
|
|
351
|
+
puts_verbose "lint_#{language}_exit: #{report.fetch( :exit_code )}"
|
|
352
|
+
puts_verbose "lint_#{language}_reason: #{report.fetch( :reason )}" unless report.fetch( :reason ).nil?
|
|
328
353
|
report
|
|
329
354
|
end
|
|
330
355
|
|
|
@@ -405,7 +430,7 @@ module Carson
|
|
|
405
430
|
unless gh_available?
|
|
406
431
|
report[ :status ] = "skipped"
|
|
407
432
|
report[ :skip_reason ] = "gh CLI not available in PATH"
|
|
408
|
-
|
|
433
|
+
puts_verbose "baseline: SKIP (#{report.fetch( :skip_reason )})"
|
|
409
434
|
return report
|
|
410
435
|
end
|
|
411
436
|
owner, repo = repository_coordinates
|
|
@@ -455,32 +480,32 @@ module Carson
|
|
|
455
480
|
report[ :status ] = "block" if report.fetch( :pending_count ).positive?
|
|
456
481
|
report[ :status ] = "block" if report.fetch( :no_check_evidence )
|
|
457
482
|
report[ :status ] = "attention" if report.fetch( :status ) == "ok" && ( report.fetch( :advisory_failing_count ).positive? || report.fetch( :advisory_pending_count ).positive? )
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
report.fetch( :failing ).each { |entry|
|
|
468
|
-
report.fetch( :pending ).each { |entry|
|
|
469
|
-
report.fetch( :advisory_failing ).each { |entry|
|
|
470
|
-
report.fetch( :advisory_pending ).each { |entry|
|
|
483
|
+
puts_verbose "default_branch_repository: #{report.fetch( :repository )}"
|
|
484
|
+
puts_verbose "default_branch_name: #{report.fetch( :default_branch )}"
|
|
485
|
+
puts_verbose "default_branch_head_sha: #{report.fetch( :head_sha )}"
|
|
486
|
+
puts_verbose "default_branch_workflows_total: #{report.fetch( :workflows_total )}"
|
|
487
|
+
puts_verbose "default_branch_check_runs_total: #{report.fetch( :check_runs_total )}"
|
|
488
|
+
puts_verbose "default_branch_failing: #{report.fetch( :failing_count )}"
|
|
489
|
+
puts_verbose "default_branch_pending: #{report.fetch( :pending_count )}"
|
|
490
|
+
puts_verbose "default_branch_advisory_failing: #{report.fetch( :advisory_failing_count )}"
|
|
491
|
+
puts_verbose "default_branch_advisory_pending: #{report.fetch( :advisory_pending_count )}"
|
|
492
|
+
report.fetch( :failing ).each { |entry| puts_verbose "default_branch_check_fail: #{entry.fetch( :workflow )} / #{entry.fetch( :name )} #{entry.fetch( :link )}".strip }
|
|
493
|
+
report.fetch( :pending ).each { |entry| puts_verbose "default_branch_check_pending: #{entry.fetch( :workflow )} / #{entry.fetch( :name )} #{entry.fetch( :link )}".strip }
|
|
494
|
+
report.fetch( :advisory_failing ).each { |entry| puts_verbose "default_branch_check_advisory_fail: #{entry.fetch( :workflow )} / #{entry.fetch( :name )} (advisory) #{entry.fetch( :link )}".strip }
|
|
495
|
+
report.fetch( :advisory_pending ).each { |entry| puts_verbose "default_branch_check_advisory_pending: #{entry.fetch( :workflow )} / #{entry.fetch( :name )} (advisory) #{entry.fetch( :link )}".strip }
|
|
471
496
|
if report.fetch( :no_check_evidence )
|
|
472
|
-
|
|
497
|
+
puts_verbose "ACTION: default branch has workflow files but no check-runs; align workflow triggers and branch protection check names."
|
|
473
498
|
end
|
|
474
499
|
report
|
|
475
500
|
rescue JSON::ParserError => e
|
|
476
501
|
report[ :status ] = "skipped"
|
|
477
502
|
report[ :skip_reason ] = "invalid gh JSON response (#{e.message})"
|
|
478
|
-
|
|
503
|
+
puts_verbose "baseline: SKIP (#{report.fetch( :skip_reason )})"
|
|
479
504
|
report
|
|
480
505
|
rescue StandardError => e
|
|
481
506
|
report[ :status ] = "skipped"
|
|
482
507
|
report[ :skip_reason ] = e.message
|
|
483
|
-
|
|
508
|
+
puts_verbose "baseline: SKIP (#{report.fetch( :skip_reason )})"
|
|
484
509
|
report
|
|
485
510
|
end
|
|
486
511
|
|
|
@@ -577,10 +602,10 @@ module Carson
|
|
|
577
602
|
# Writes monitor report artefacts and prints their locations.
|
|
578
603
|
def write_and_print_pr_monitor_report( report: )
|
|
579
604
|
markdown_path, json_path = write_pr_monitor_report( report: report )
|
|
580
|
-
|
|
581
|
-
|
|
605
|
+
puts_verbose "report_markdown: #{markdown_path}"
|
|
606
|
+
puts_verbose "report_json: #{json_path}"
|
|
582
607
|
rescue StandardError => e
|
|
583
|
-
|
|
608
|
+
puts_verbose "report_write: SKIP (#{e.message})"
|
|
584
609
|
end
|
|
585
610
|
|
|
586
611
|
# Persists report in both machine-readable JSON and human-readable Markdown.
|
|
@@ -703,28 +728,29 @@ module Carson
|
|
|
703
728
|
return { status: "ok", split_required: false } if files.empty?
|
|
704
729
|
|
|
705
730
|
scope = scope_integrity_status( files: files, branch: current_branch )
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
scope.fetch( :unmatched_paths ).
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
731
|
+
puts_verbose ""
|
|
732
|
+
puts_verbose "[Scope Integrity Guard]"
|
|
733
|
+
puts_verbose "scope_file_source: #{files_source}"
|
|
734
|
+
puts_verbose "scope_file_count: #{files.count}"
|
|
735
|
+
puts_verbose "branch: #{scope.fetch( :branch )}"
|
|
736
|
+
puts_verbose "scope_basis: changed_paths_only"
|
|
737
|
+
puts_verbose "detected_groups: #{scope.fetch( :detected_groups ).sort.join( ', ' )}"
|
|
738
|
+
puts_verbose "core_groups: #{scope.fetch( :core_groups ).empty? ? 'none' : scope.fetch( :core_groups ).sort.join( ', ' )}"
|
|
739
|
+
puts_verbose "non_doc_groups: #{scope.fetch( :non_doc_groups ).empty? ? 'none' : scope.fetch( :non_doc_groups ).sort.join( ', ' )}"
|
|
740
|
+
puts_verbose "docs_only_changes: #{scope.fetch( :docs_only )}"
|
|
741
|
+
puts_verbose "unmatched_paths_count: #{scope.fetch( :unmatched_paths ).count}"
|
|
742
|
+
scope.fetch( :unmatched_paths ).each { |path| puts_verbose "unmatched_path: #{path}" }
|
|
743
|
+
puts_verbose "violating_files_count: #{scope.fetch( :violating_files ).count}"
|
|
744
|
+
scope.fetch( :violating_files ).each { |path| puts_verbose "violating_file: #{path} (group=#{scope.fetch( :grouped_paths ).fetch( path )})" }
|
|
745
|
+
puts_verbose "checklist_single_business_intent: pass"
|
|
746
|
+
puts_verbose "checklist_single_scope_group: #{scope.fetch( :split_required ) ? 'advisory' : 'pass'}"
|
|
747
|
+
puts_verbose "checklist_cross_boundary_changes_justified: #{( scope.fetch( :split_required ) || scope.fetch( :misc_present ) ) ? 'advisory' : 'pass'}"
|
|
722
748
|
if scope.fetch( :split_required )
|
|
723
|
-
|
|
749
|
+
puts_verbose "ACTION: multiple module groups detected (informational only)."
|
|
724
750
|
elsif scope.fetch( :misc_present )
|
|
725
|
-
|
|
751
|
+
puts_verbose "ACTION: unmatched paths detected; classify via scope.path_groups for stricter module checks."
|
|
726
752
|
else
|
|
727
|
-
|
|
753
|
+
puts_verbose "ACTION: scope integrity is within commit policy."
|
|
728
754
|
end
|
|
729
755
|
{ status: scope.fetch( :status ), split_required: scope.fetch( :split_required ) }
|
|
730
756
|
end
|
|
@@ -83,7 +83,8 @@ module Carson
|
|
|
83
83
|
|
|
84
84
|
# Standalone housekeep: sync + prune.
|
|
85
85
|
def housekeep!
|
|
86
|
-
|
|
86
|
+
puts_verbose ""
|
|
87
|
+
puts_verbose "[Housekeep]"
|
|
87
88
|
sync_status = sync!
|
|
88
89
|
if sync_status != EXIT_OK
|
|
89
90
|
puts_line "housekeep: sync returned #{sync_status}; skipping prune."
|
|
@@ -571,8 +572,8 @@ module Carson
|
|
|
571
572
|
md_path = File.join( report_dir, GOVERN_REPORT_MD )
|
|
572
573
|
File.write( json_path, JSON.pretty_generate( report ) )
|
|
573
574
|
File.write( md_path, render_govern_markdown( report: report ) )
|
|
574
|
-
|
|
575
|
-
|
|
575
|
+
puts_verbose "report_json: #{json_path}"
|
|
576
|
+
puts_verbose "report_markdown: #{md_path}"
|
|
576
577
|
end
|
|
577
578
|
|
|
578
579
|
def render_govern_markdown( report: )
|
|
@@ -632,7 +633,11 @@ module Carson
|
|
|
632
633
|
end
|
|
633
634
|
|
|
634
635
|
repos_count = Array( report[ :repos ] ).length
|
|
635
|
-
|
|
636
|
+
if verbose?
|
|
637
|
+
puts_line "govern_summary: repos=#{repos_count} prs=#{total_prs} ready=#{ready_count} blocked=#{blocked_count}"
|
|
638
|
+
else
|
|
639
|
+
puts_line "Govern: #{repos_count} repo#{plural_suffix( count: repos_count )}, #{total_prs} PR#{plural_suffix( count: total_prs )} (#{ready_count} ready, #{blocked_count} blocked)"
|
|
640
|
+
end
|
|
636
641
|
end
|
|
637
642
|
end
|
|
638
643
|
|
data/lib/carson/runtime/lint.rb
CHANGED
|
@@ -7,7 +7,8 @@ module Carson
|
|
|
7
7
|
module Lint
|
|
8
8
|
# Prepares canonical lint policy files under ~/.carson/lint from an explicit source.
|
|
9
9
|
def lint_setup!( source:, ref: "main", force: false )
|
|
10
|
-
|
|
10
|
+
puts_verbose ""
|
|
11
|
+
puts_verbose "[Lint Setup]"
|
|
11
12
|
source_text = source.to_s.strip
|
|
12
13
|
if source_text.empty?
|
|
13
14
|
puts_line "ERROR: lint setup requires --source <path-or-git-url>."
|
|
@@ -29,12 +30,12 @@ module Carson
|
|
|
29
30
|
target_coding_dir: target_coding_dir,
|
|
30
31
|
force: force
|
|
31
32
|
)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
puts_verbose "lint_setup_source: #{source_text}"
|
|
34
|
+
puts_verbose "lint_setup_ref: #{ref_text}" if lint_source_git_url?( source: source_text )
|
|
35
|
+
puts_verbose "lint_setup_target: #{target_coding_dir}"
|
|
36
|
+
puts_verbose "lint_setup_created: #{copy_result.fetch( :created )}"
|
|
37
|
+
puts_verbose "lint_setup_updated: #{copy_result.fetch( :updated )}"
|
|
38
|
+
puts_verbose "lint_setup_skipped: #{copy_result.fetch( :skipped )}"
|
|
38
39
|
|
|
39
40
|
missing_policy = missing_lint_policy_files
|
|
40
41
|
if missing_policy.empty?
|
|
@@ -43,7 +44,7 @@ module Carson
|
|
|
43
44
|
end
|
|
44
45
|
|
|
45
46
|
missing_policy.each do |entry|
|
|
46
|
-
|
|
47
|
+
puts_verbose "missing_lint_policy_file: language=#{entry.fetch( :language )} path=#{entry.fetch( :path )}"
|
|
47
48
|
end
|
|
48
49
|
puts_line "ACTION: update source CODING policy files, rerun carson lint setup, then rerun carson audit."
|
|
49
50
|
EXIT_ERROR
|
data/lib/carson/runtime/local.rb
CHANGED
|
@@ -43,12 +43,19 @@ module Carson
|
|
|
43
43
|
return prune_no_stale_branches if stale_branches.empty?
|
|
44
44
|
|
|
45
45
|
counters = prune_stale_branch_entries( stale_branches: stale_branches, active_branch: active_branch )
|
|
46
|
-
|
|
46
|
+
puts_verbose "prune_summary: deleted=#{counters.fetch( :deleted )} skipped=#{counters.fetch( :skipped )}"
|
|
47
|
+
unless verbose?
|
|
48
|
+
puts_line "Pruned #{counters.fetch( :deleted )} stale branch#{plural_suffix( count: counters.fetch( :deleted ) )}."
|
|
49
|
+
end
|
|
47
50
|
EXIT_OK
|
|
48
51
|
end
|
|
49
52
|
|
|
50
53
|
def prune_no_stale_branches
|
|
51
|
-
|
|
54
|
+
if verbose?
|
|
55
|
+
puts_line "OK: no stale local branches tracking deleted #{config.git_remote} branches."
|
|
56
|
+
else
|
|
57
|
+
puts_line "No stale branches."
|
|
58
|
+
end
|
|
52
59
|
EXIT_OK
|
|
53
60
|
end
|
|
54
61
|
|
|
@@ -72,7 +79,7 @@ module Carson
|
|
|
72
79
|
|
|
73
80
|
def prune_skip_stale_branch( type:, branch:, upstream: )
|
|
74
81
|
status = type == :protected ? "skip_protected_branch" : "skip_current_branch"
|
|
75
|
-
|
|
82
|
+
puts_verbose "#{status}: #{branch} (upstream=#{upstream})"
|
|
76
83
|
:skipped
|
|
77
84
|
end
|
|
78
85
|
|
|
@@ -89,8 +96,8 @@ module Carson
|
|
|
89
96
|
end
|
|
90
97
|
|
|
91
98
|
def prune_safe_delete_success( branch:, upstream:, stdout_text: )
|
|
92
|
-
out.print stdout_text
|
|
93
|
-
|
|
99
|
+
out.print stdout_text if verbose? && !stdout_text.empty?
|
|
100
|
+
puts_verbose "deleted_local_branch: #{branch} (upstream=#{upstream})"
|
|
94
101
|
:deleted
|
|
95
102
|
end
|
|
96
103
|
|
|
@@ -108,20 +115,20 @@ module Carson
|
|
|
108
115
|
end
|
|
109
116
|
|
|
110
117
|
def prune_force_delete_success( branch:, upstream:, merged_pr:, force_stdout: )
|
|
111
|
-
out.print force_stdout
|
|
112
|
-
|
|
118
|
+
out.print force_stdout if verbose? && !force_stdout.empty?
|
|
119
|
+
puts_verbose "deleted_local_branch_force: #{branch} (upstream=#{upstream}) merged_pr=#{merged_pr.fetch( :url )}"
|
|
113
120
|
:deleted
|
|
114
121
|
end
|
|
115
122
|
|
|
116
123
|
def prune_force_delete_failed( branch:, upstream:, force_stderr: )
|
|
117
124
|
force_error_text = normalise_branch_delete_error( error_text: force_stderr )
|
|
118
|
-
|
|
125
|
+
puts_verbose "fail_force_delete_branch: #{branch} (upstream=#{upstream}) reason=#{force_error_text}"
|
|
119
126
|
:skipped
|
|
120
127
|
end
|
|
121
128
|
|
|
122
129
|
def prune_force_delete_skipped( branch:, upstream:, delete_error_text:, force_error: )
|
|
123
|
-
|
|
124
|
-
|
|
130
|
+
puts_verbose "skip_delete_branch: #{branch} (upstream=#{upstream}) reason=#{delete_error_text}"
|
|
131
|
+
puts_verbose "skip_force_delete_branch: #{branch} (upstream=#{upstream}) reason=#{force_error}" unless force_error.to_s.strip.empty?
|
|
125
132
|
:skipped
|
|
126
133
|
end
|
|
127
134
|
|
|
@@ -153,12 +160,15 @@ module Carson
|
|
|
153
160
|
target_path = File.join( hooks_dir, hook_name )
|
|
154
161
|
FileUtils.cp( source_path, target_path )
|
|
155
162
|
FileUtils.chmod( 0o755, target_path )
|
|
156
|
-
|
|
163
|
+
puts_verbose "hook_written: #{relative_path( target_path )}"
|
|
157
164
|
end
|
|
158
165
|
git_system!( "config", "core.hooksPath", hooks_dir )
|
|
159
166
|
File.write( File.join( hooks_dir, "workflow_style" ), config.workflow_style )
|
|
160
|
-
|
|
161
|
-
|
|
167
|
+
puts_verbose "configured_hooks_path: #{hooks_dir}"
|
|
168
|
+
unless verbose?
|
|
169
|
+
puts_line "Hooks installed (#{config.required_hooks.count} hooks)."
|
|
170
|
+
return EXIT_OK
|
|
171
|
+
end
|
|
162
172
|
|
|
163
173
|
inspect!
|
|
164
174
|
end
|
|
@@ -188,8 +198,6 @@ module Carson
|
|
|
188
198
|
end
|
|
189
199
|
|
|
190
200
|
onboard_apply!
|
|
191
|
-
ensure
|
|
192
|
-
@concise = false
|
|
193
201
|
end
|
|
194
202
|
|
|
195
203
|
# Re-applies hooks, templates, and audit after upgrading Carson.
|
|
@@ -197,29 +205,52 @@ module Carson
|
|
|
197
205
|
fingerprint_status = block_if_outsider_fingerprints!
|
|
198
206
|
return fingerprint_status unless fingerprint_status.nil?
|
|
199
207
|
|
|
200
|
-
print_header "Refresh"
|
|
201
208
|
unless inside_git_work_tree?
|
|
202
209
|
puts_line "ERROR: #{repo_root} is not a git repository."
|
|
203
210
|
return EXIT_ERROR
|
|
204
211
|
end
|
|
205
|
-
|
|
212
|
+
|
|
213
|
+
if verbose?
|
|
214
|
+
puts_verbose ""
|
|
215
|
+
puts_verbose "[Refresh]"
|
|
216
|
+
hook_status = prepare!
|
|
217
|
+
return hook_status unless hook_status == EXIT_OK
|
|
218
|
+
|
|
219
|
+
template_status = template_apply!
|
|
220
|
+
return template_status unless template_status == EXIT_OK
|
|
221
|
+
|
|
222
|
+
audit_status = audit!
|
|
223
|
+
if audit_status == EXIT_OK
|
|
224
|
+
puts_line "OK: Carson refresh completed for #{repo_root}."
|
|
225
|
+
elsif audit_status == EXIT_BLOCK
|
|
226
|
+
puts_line "BLOCK: Carson refresh completed with policy blocks; resolve and rerun carson audit."
|
|
227
|
+
end
|
|
228
|
+
return audit_status
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
puts_line "Refresh"
|
|
232
|
+
hook_status = with_captured_output { prepare! }
|
|
206
233
|
return hook_status unless hook_status == EXIT_OK
|
|
234
|
+
puts_line "Hooks installed (#{config.required_hooks.count} hooks)."
|
|
207
235
|
|
|
208
|
-
|
|
236
|
+
template_drift_count = template_results.count { |entry| entry.fetch( :status ) != "ok" }
|
|
237
|
+
template_status = with_captured_output { template_apply! }
|
|
209
238
|
return template_status unless template_status == EXIT_OK
|
|
239
|
+
if template_drift_count.positive?
|
|
240
|
+
puts_line "Templates applied (#{template_drift_count} updated)."
|
|
241
|
+
else
|
|
242
|
+
puts_line "Templates in sync."
|
|
243
|
+
end
|
|
210
244
|
|
|
211
245
|
audit_status = audit!
|
|
212
|
-
|
|
213
|
-
puts_line "OK: Carson refresh completed for #{repo_root}."
|
|
214
|
-
elsif audit_status == EXIT_BLOCK
|
|
215
|
-
puts_line "BLOCK: Carson refresh completed with policy blocks; resolve and rerun carson audit."
|
|
216
|
-
end
|
|
246
|
+
puts_line "Refresh complete."
|
|
217
247
|
audit_status
|
|
218
248
|
end
|
|
219
249
|
|
|
220
250
|
# Removes Carson-managed repository integration so a host repository can retire Carson cleanly.
|
|
221
251
|
def offboard!
|
|
222
|
-
|
|
252
|
+
puts_verbose ""
|
|
253
|
+
puts_verbose "[Offboard]"
|
|
223
254
|
unless inside_git_work_tree?
|
|
224
255
|
puts_line "ERROR: #{repo_root} is not a git repository."
|
|
225
256
|
return EXIT_ERROR
|
|
@@ -233,16 +264,20 @@ module Carson
|
|
|
233
264
|
absolute = resolve_repo_path!( relative_path: relative, label: "offboard target #{relative}" )
|
|
234
265
|
if File.exist?( absolute )
|
|
235
266
|
FileUtils.rm_rf( absolute )
|
|
236
|
-
|
|
267
|
+
puts_verbose "removed_path: #{relative}"
|
|
237
268
|
removed_count += 1
|
|
238
269
|
else
|
|
239
|
-
|
|
270
|
+
puts_verbose "skip_missing_path: #{relative}"
|
|
240
271
|
missing_count += 1
|
|
241
272
|
end
|
|
242
273
|
end
|
|
243
274
|
remove_empty_offboard_directories!
|
|
244
|
-
|
|
245
|
-
|
|
275
|
+
puts_verbose "offboard_summary: removed=#{removed_count} missing=#{missing_count}"
|
|
276
|
+
if verbose?
|
|
277
|
+
puts_line "OK: Carson offboard completed for #{repo_root}."
|
|
278
|
+
else
|
|
279
|
+
puts_line "Removed #{removed_count} file#{plural_suffix( count: removed_count )}. Offboard complete."
|
|
280
|
+
end
|
|
246
281
|
EXIT_OK
|
|
247
282
|
end
|
|
248
283
|
|
|
@@ -251,9 +286,13 @@ module Carson
|
|
|
251
286
|
fingerprint_status = block_if_outsider_fingerprints!
|
|
252
287
|
return fingerprint_status unless fingerprint_status.nil?
|
|
253
288
|
|
|
254
|
-
|
|
289
|
+
puts_verbose ""
|
|
290
|
+
puts_verbose "[Inspect]"
|
|
255
291
|
ok = hooks_health_report( strict: true )
|
|
256
|
-
|
|
292
|
+
puts_verbose( ok ? "status: ok" : "status: block" )
|
|
293
|
+
unless verbose?
|
|
294
|
+
puts_line( ok ? "Hooks: ok" : "Hooks: block" )
|
|
295
|
+
end
|
|
257
296
|
ok ? EXIT_OK : EXIT_BLOCK
|
|
258
297
|
end
|
|
259
298
|
|
|
@@ -262,14 +301,24 @@ module Carson
|
|
|
262
301
|
fingerprint_status = block_if_outsider_fingerprints!
|
|
263
302
|
return fingerprint_status unless fingerprint_status.nil?
|
|
264
303
|
|
|
265
|
-
|
|
304
|
+
puts_verbose ""
|
|
305
|
+
puts_verbose "[Template Sync Check]"
|
|
266
306
|
results = template_results
|
|
267
307
|
drift_count = results.count { |entry| entry.fetch( :status ) == "drift" }
|
|
268
308
|
error_count = results.count { |entry| entry.fetch( :status ) == "error" }
|
|
269
309
|
results.each do |entry|
|
|
270
|
-
|
|
310
|
+
puts_verbose "template_file: #{entry.fetch( :file )} status=#{entry.fetch( :status )} reason=#{entry.fetch( :reason )}"
|
|
311
|
+
end
|
|
312
|
+
puts_verbose "template_summary: total=#{results.count} drift=#{drift_count} error=#{error_count}"
|
|
313
|
+
unless verbose?
|
|
314
|
+
if drift_count.positive?
|
|
315
|
+
drift_files = results.select { |entry| entry.fetch( :status ) == "drift" }.map { |entry| entry.fetch( :file ) }
|
|
316
|
+
puts_line "Templates: #{drift_count} of #{results.count} drifted"
|
|
317
|
+
drift_files.each { |file| puts_line " #{file}" }
|
|
318
|
+
else
|
|
319
|
+
puts_line "Templates: #{results.count} files in sync"
|
|
320
|
+
end
|
|
271
321
|
end
|
|
272
|
-
puts_line "template_summary: total=#{results.count} drift=#{drift_count} error=#{error_count}"
|
|
273
322
|
return EXIT_ERROR if error_count.positive?
|
|
274
323
|
|
|
275
324
|
drift_count.positive? ? EXIT_BLOCK : EXIT_OK
|
|
@@ -280,29 +329,37 @@ module Carson
|
|
|
280
329
|
fingerprint_status = block_if_outsider_fingerprints!
|
|
281
330
|
return fingerprint_status unless fingerprint_status.nil?
|
|
282
331
|
|
|
283
|
-
|
|
332
|
+
puts_verbose ""
|
|
333
|
+
puts_verbose "[Template Sync Apply]"
|
|
284
334
|
results = template_results
|
|
285
335
|
applied = 0
|
|
286
336
|
results.each do |entry|
|
|
287
337
|
if entry.fetch( :status ) == "error"
|
|
288
|
-
|
|
338
|
+
puts_verbose "template_file: #{entry.fetch( :file )} status=error reason=#{entry.fetch( :reason )}"
|
|
289
339
|
next
|
|
290
340
|
end
|
|
291
341
|
|
|
292
342
|
file_path = File.join( repo_root, entry.fetch( :file ) )
|
|
293
343
|
if entry.fetch( :status ) == "ok"
|
|
294
|
-
|
|
344
|
+
puts_verbose "template_file: #{entry.fetch( :file )} status=ok reason=in_sync"
|
|
295
345
|
next
|
|
296
346
|
end
|
|
297
347
|
|
|
298
348
|
FileUtils.mkdir_p( File.dirname( file_path ) )
|
|
299
349
|
File.write( file_path, entry.fetch( :applied_content ) )
|
|
300
|
-
|
|
350
|
+
puts_verbose "template_file: #{entry.fetch( :file )} status=updated reason=#{entry.fetch( :reason )}"
|
|
301
351
|
applied += 1
|
|
302
352
|
end
|
|
303
353
|
|
|
304
354
|
error_count = results.count { |entry| entry.fetch( :status ) == "error" }
|
|
305
|
-
|
|
355
|
+
puts_verbose "template_apply_summary: updated=#{applied} error=#{error_count}"
|
|
356
|
+
unless verbose?
|
|
357
|
+
if applied.positive?
|
|
358
|
+
puts_line "Templates applied (#{applied} updated)."
|
|
359
|
+
else
|
|
360
|
+
puts_line "Templates in sync."
|
|
361
|
+
end
|
|
362
|
+
end
|
|
306
363
|
error_count.positive? ? EXIT_ERROR : EXIT_OK
|
|
307
364
|
end
|
|
308
365
|
|
|
@@ -363,9 +420,9 @@ module Carson
|
|
|
363
420
|
def print_hooks_path_status( configured:, expected: )
|
|
364
421
|
configured_abs = configured.nil? ? nil : File.expand_path( configured )
|
|
365
422
|
hooks_path_ok = configured_abs == expected
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
423
|
+
puts_verbose "hooks_path: #{configured || '(unset)'}"
|
|
424
|
+
puts_verbose "hooks_path_expected: #{expected}"
|
|
425
|
+
puts_verbose( hooks_path_ok ? "hooks_path_status: ok" : "hooks_path_status: attention" )
|
|
369
426
|
hooks_path_ok
|
|
370
427
|
end
|
|
371
428
|
|
|
@@ -374,7 +431,7 @@ module Carson
|
|
|
374
431
|
exists = File.file?( path )
|
|
375
432
|
symlink = File.symlink?( path )
|
|
376
433
|
executable = exists && !symlink && File.executable?( path )
|
|
377
|
-
|
|
434
|
+
puts_verbose "hook_file: #{relative_path( path )} exists=#{exists} symlink=#{symlink} executable=#{executable}"
|
|
378
435
|
end
|
|
379
436
|
end
|
|
380
437
|
|
|
@@ -399,13 +456,13 @@ module Carson
|
|
|
399
456
|
if strict && !hooks_path_ok
|
|
400
457
|
configured_text = configured.to_s.strip
|
|
401
458
|
if configured_text.empty?
|
|
402
|
-
|
|
459
|
+
puts_verbose "ACTION: hooks path is unset (expected=#{expected})."
|
|
403
460
|
else
|
|
404
|
-
|
|
461
|
+
puts_verbose "ACTION: hooks path mismatch (configured=#{configured_text}, expected=#{expected})."
|
|
405
462
|
end
|
|
406
463
|
end
|
|
407
464
|
message = strict ? "ACTION: run carson prepare to align hooks with Carson #{Carson::VERSION}." : "ACTION: run carson prepare to enforce local main protections."
|
|
408
|
-
|
|
465
|
+
puts_verbose message
|
|
409
466
|
end
|
|
410
467
|
|
|
411
468
|
# Returns ahead/behind counts for local main versus configured remote main.
|
|
@@ -604,17 +661,17 @@ module Carson
|
|
|
604
661
|
def disable_carson_hooks_path!
|
|
605
662
|
configured = configured_hooks_path
|
|
606
663
|
if configured.nil?
|
|
607
|
-
|
|
664
|
+
puts_verbose "hooks_path: (unset)"
|
|
608
665
|
return EXIT_OK
|
|
609
666
|
end
|
|
610
|
-
|
|
667
|
+
puts_verbose "hooks_path: #{configured}"
|
|
611
668
|
configured_abs = File.expand_path( configured, repo_root )
|
|
612
669
|
unless carson_managed_hooks_path?( configured_abs: configured_abs )
|
|
613
|
-
|
|
670
|
+
puts_verbose "hooks_path_kept: #{configured} (not Carson-managed)"
|
|
614
671
|
return EXIT_OK
|
|
615
672
|
end
|
|
616
673
|
git_system!( "config", "--unset", "core.hooksPath" )
|
|
617
|
-
|
|
674
|
+
puts_verbose "hooks_path_unset: core.hooksPath"
|
|
618
675
|
EXIT_OK
|
|
619
676
|
rescue StandardError => e
|
|
620
677
|
puts_line "ERROR: unable to update core.hooksPath (#{e.message})"
|
|
@@ -660,14 +717,14 @@ module Carson
|
|
|
660
717
|
next unless Dir.empty?( absolute )
|
|
661
718
|
|
|
662
719
|
Dir.rmdir( absolute )
|
|
663
|
-
|
|
720
|
+
puts_verbose "removed_empty_dir: #{relative}"
|
|
664
721
|
end
|
|
665
722
|
end
|
|
666
723
|
|
|
667
724
|
# Verifies configured remote exists and logs status without mutating remotes.
|
|
668
725
|
def report_detected_remote!
|
|
669
726
|
if git_remote_exists?( remote_name: config.git_remote )
|
|
670
|
-
|
|
727
|
+
puts_verbose "remote_ok: #{config.git_remote}"
|
|
671
728
|
else
|
|
672
729
|
puts_line "WARN: remote '#{config.git_remote}' not found; run carson setup to configure."
|
|
673
730
|
end
|
|
@@ -675,14 +732,12 @@ module Carson
|
|
|
675
732
|
|
|
676
733
|
# Concise onboard orchestration: hooks, templates, remote, audit, guidance.
|
|
677
734
|
def onboard_apply!
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
hook_status = prepare!
|
|
735
|
+
hook_status = with_captured_output { prepare! }
|
|
681
736
|
return hook_status unless hook_status == EXIT_OK
|
|
682
737
|
puts_line "Hooks installed (#{config.required_hooks.count} hooks)."
|
|
683
738
|
|
|
684
739
|
template_drift_count = template_results.count { |entry| entry.fetch( :status ) != "ok" }
|
|
685
|
-
template_status = template_apply!
|
|
740
|
+
template_status = with_captured_output { template_apply! }
|
|
686
741
|
return template_status unless template_status == EXIT_OK
|
|
687
742
|
if template_drift_count.positive?
|
|
688
743
|
puts_line "Templates synced (#{template_drift_count} file#{plural_suffix( count: template_drift_count )} updated)."
|
|
@@ -8,10 +8,10 @@ module Carson
|
|
|
8
8
|
return unless config.review_wait_seconds.positive?
|
|
9
9
|
quick = review_gate_snapshot( owner: owner, repo: repo, pr_number: pr_number )
|
|
10
10
|
if quick[ :unresolved_threads ].empty? && quick[ :unacknowledged_actionable ].empty?
|
|
11
|
-
|
|
11
|
+
puts_verbose "warmup_skip: all threads resolved"
|
|
12
12
|
return quick
|
|
13
13
|
end
|
|
14
|
-
|
|
14
|
+
puts_verbose "warmup_wait_seconds: #{config.review_wait_seconds}"
|
|
15
15
|
sleep config.review_wait_seconds
|
|
16
16
|
nil
|
|
17
17
|
end
|
|
@@ -19,7 +19,7 @@ module Carson
|
|
|
19
19
|
# Poll delay between consecutive snapshot reads during convergence checks.
|
|
20
20
|
def wait_for_review_poll
|
|
21
21
|
return unless config.review_poll_seconds.positive?
|
|
22
|
-
|
|
22
|
+
puts_verbose "poll_wait_seconds: #{config.review_poll_seconds}"
|
|
23
23
|
sleep config.review_poll_seconds
|
|
24
24
|
end
|
|
25
25
|
|
|
@@ -173,10 +173,10 @@ module Carson
|
|
|
173
173
|
json_name: REVIEW_GATE_REPORT_JSON,
|
|
174
174
|
renderer: method( :render_review_gate_markdown )
|
|
175
175
|
)
|
|
176
|
-
|
|
177
|
-
|
|
176
|
+
puts_verbose "review_gate_report_markdown: #{markdown_path}"
|
|
177
|
+
puts_verbose "review_gate_report_json: #{json_path}"
|
|
178
178
|
rescue StandardError => e
|
|
179
|
-
|
|
179
|
+
puts_verbose "review_gate_report_write: SKIP (#{e.message})"
|
|
180
180
|
end
|
|
181
181
|
|
|
182
182
|
# Human-readable review gate report for merge-readiness evidence.
|
|
@@ -202,10 +202,10 @@ module Carson
|
|
|
202
202
|
json_name: REVIEW_SWEEP_REPORT_JSON,
|
|
203
203
|
renderer: method( :render_review_sweep_markdown )
|
|
204
204
|
)
|
|
205
|
-
|
|
206
|
-
|
|
205
|
+
puts_verbose "review_sweep_report_markdown: #{markdown_path}"
|
|
206
|
+
puts_verbose "review_sweep_report_json: #{json_path}"
|
|
207
207
|
rescue StandardError => e
|
|
208
|
-
|
|
208
|
+
puts_verbose "review_sweep_report_write: SKIP (#{e.message})"
|
|
209
209
|
end
|
|
210
210
|
|
|
211
211
|
# Human-readable scheduled sweep report.
|
|
@@ -16,7 +16,11 @@ module Carson
|
|
|
16
16
|
def review_gate!
|
|
17
17
|
fingerprint_status = block_if_outsider_fingerprints!
|
|
18
18
|
return fingerprint_status unless fingerprint_status.nil?
|
|
19
|
-
|
|
19
|
+
puts_verbose ""
|
|
20
|
+
puts_verbose "[Review Gate]"
|
|
21
|
+
unless verbose?
|
|
22
|
+
puts_line "Review Gate"
|
|
23
|
+
end
|
|
20
24
|
unless gh_available?
|
|
21
25
|
puts_line "ERROR: gh CLI not available in PATH."
|
|
22
26
|
return EXIT_ERROR
|
|
@@ -67,13 +71,13 @@ module Carson
|
|
|
67
71
|
snapshot = review_gate_snapshot( owner: owner, repo: repo, pr_number: pr_summary.fetch( :number ) )
|
|
68
72
|
last_snapshot = snapshot
|
|
69
73
|
signature = review_gate_signature( snapshot: snapshot )
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
+
puts_verbose "poll_attempt: #{poll_attempts}/#{config.review_max_polls}"
|
|
75
|
+
puts_verbose "latest_activity: #{snapshot.fetch( :latest_activity ) || 'unknown'}"
|
|
76
|
+
puts_verbose "unresolved_threads: #{snapshot.fetch( :unresolved_threads ).count}"
|
|
77
|
+
puts_verbose "unacknowledged_actionable: #{snapshot.fetch( :unacknowledged_actionable ).count}"
|
|
74
78
|
if !last_signature.nil? && signature == last_signature
|
|
75
79
|
converged = true
|
|
76
|
-
|
|
80
|
+
puts_verbose "convergence: stable"
|
|
77
81
|
break
|
|
78
82
|
end
|
|
79
83
|
last_signature = signature
|
|
@@ -110,6 +114,9 @@ module Carson
|
|
|
110
114
|
unacknowledged_actionable: last_snapshot.fetch( :unacknowledged_actionable )
|
|
111
115
|
}
|
|
112
116
|
write_review_gate_report( report: report )
|
|
117
|
+
unless verbose?
|
|
118
|
+
puts_line "Polling... (converged after #{poll_attempts} attempt#{plural_suffix( count: poll_attempts )})"
|
|
119
|
+
end
|
|
113
120
|
if block_reasons.empty?
|
|
114
121
|
puts_line "OK: review gate passed."
|
|
115
122
|
return EXIT_OK
|
|
@@ -128,7 +135,8 @@ module Carson
|
|
|
128
135
|
def review_sweep!
|
|
129
136
|
fingerprint_status = block_if_outsider_fingerprints!
|
|
130
137
|
return fingerprint_status unless fingerprint_status.nil?
|
|
131
|
-
|
|
138
|
+
puts_verbose ""
|
|
139
|
+
puts_verbose "[Review Sweep]"
|
|
132
140
|
unless gh_available?
|
|
133
141
|
puts_line "ERROR: gh CLI not available in PATH."
|
|
134
142
|
return EXIT_ERROR
|
|
@@ -137,8 +145,8 @@ module Carson
|
|
|
137
145
|
owner, repo = repository_coordinates
|
|
138
146
|
cutoff_time = Time.now.utc - ( config.review_sweep_window_days * 86_400 )
|
|
139
147
|
pull_requests = recent_pull_requests_for_sweep( owner: owner, repo: repo, cutoff_time: cutoff_time )
|
|
140
|
-
|
|
141
|
-
|
|
148
|
+
puts_verbose "window_days: #{config.review_sweep_window_days}"
|
|
149
|
+
puts_verbose "candidate_prs: #{pull_requests.count}"
|
|
142
150
|
findings = []
|
|
143
151
|
|
|
144
152
|
pull_requests.each do |entry|
|
data/lib/carson/runtime/setup.rb
CHANGED
|
@@ -4,7 +4,8 @@ module Carson
|
|
|
4
4
|
WELL_KNOWN_REMOTES = %w[origin github upstream].freeze
|
|
5
5
|
|
|
6
6
|
def setup!
|
|
7
|
-
|
|
7
|
+
puts_verbose ""
|
|
8
|
+
puts_verbose "[Setup]"
|
|
8
9
|
|
|
9
10
|
unless inside_git_work_tree?
|
|
10
11
|
puts_line "WARN: not a git repository. Skipping remote and branch detection."
|
|
@@ -43,19 +44,19 @@ module Carson
|
|
|
43
44
|
choices = {}
|
|
44
45
|
if detected && detected != config.git_remote
|
|
45
46
|
choices[ "git.remote" ] = detected
|
|
46
|
-
|
|
47
|
+
puts_verbose "detected_remote: #{detected}"
|
|
47
48
|
elsif detected
|
|
48
|
-
|
|
49
|
+
puts_verbose "detected_remote: #{detected}"
|
|
49
50
|
else
|
|
50
|
-
|
|
51
|
+
puts_verbose "detected_remote: none"
|
|
51
52
|
end
|
|
52
53
|
|
|
53
54
|
branch = detect_main_branch
|
|
54
55
|
if branch && branch != config.main_branch
|
|
55
56
|
choices[ "git.main_branch" ] = branch
|
|
56
|
-
|
|
57
|
+
puts_verbose "detected_main_branch: #{branch}"
|
|
57
58
|
elsif branch
|
|
58
|
-
|
|
59
|
+
puts_verbose "detected_main_branch: #{branch}"
|
|
59
60
|
end
|
|
60
61
|
|
|
61
62
|
write_setup_config( choices: choices )
|
|
@@ -103,8 +104,8 @@ module Carson
|
|
|
103
104
|
puts_line ""
|
|
104
105
|
puts_line "Workflow style"
|
|
105
106
|
options = [
|
|
106
|
-
{ label: "
|
|
107
|
-
{ label: "
|
|
107
|
+
{ label: "branch — enforce PR-only merges (default)", value: "branch" },
|
|
108
|
+
{ label: "trunk — commit directly to main", value: "trunk" }
|
|
108
109
|
]
|
|
109
110
|
prompt_choice( options: options, default: 0 )
|
|
110
111
|
end
|
data/lib/carson/runtime.rb
CHANGED
|
@@ -23,13 +23,13 @@ module Carson
|
|
|
23
23
|
DISPOSITION_TOKENS = %w[accepted rejected deferred].freeze
|
|
24
24
|
|
|
25
25
|
# Runtime wiring for repository context, tool paths, and output streams.
|
|
26
|
-
def initialize( repo_root:, tool_root:, out:, err:, in_stream: $stdin )
|
|
26
|
+
def initialize( repo_root:, tool_root:, out:, err:, in_stream: $stdin, verbose: false )
|
|
27
27
|
@repo_root = repo_root
|
|
28
28
|
@tool_root = tool_root
|
|
29
29
|
@out = out
|
|
30
30
|
@err = err
|
|
31
31
|
@in = in_stream
|
|
32
|
-
@
|
|
32
|
+
@verbose = verbose
|
|
33
33
|
@config = Config.load( repo_root: repo_root )
|
|
34
34
|
@git_adapter = Adapters::Git.new( repo_root: repo_root )
|
|
35
35
|
@github_adapter = Adapters::GitHub.new( repo_root: repo_root )
|
|
@@ -39,9 +39,14 @@ module Carson
|
|
|
39
39
|
|
|
40
40
|
attr_reader :repo_root, :tool_root, :out, :err, :in, :config, :git_adapter, :github_adapter
|
|
41
41
|
|
|
42
|
-
# Returns true when
|
|
43
|
-
def
|
|
44
|
-
@
|
|
42
|
+
# Returns true when full diagnostic output is enabled via --verbose.
|
|
43
|
+
def verbose?
|
|
44
|
+
@verbose
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Prints a line only when verbose mode is active.
|
|
48
|
+
def puts_verbose( message )
|
|
49
|
+
puts_line( message ) if verbose?
|
|
45
50
|
end
|
|
46
51
|
|
|
47
52
|
# Runs a block with all output captured (suppressed from the user).
|
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.
|
|
4
|
+
version: 2.10.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Hailei Wang
|
|
@@ -88,7 +88,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
88
88
|
requirements:
|
|
89
89
|
- - ">="
|
|
90
90
|
- !ruby/object:Gem::Version
|
|
91
|
-
version: '4
|
|
91
|
+
version: '3.4'
|
|
92
92
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
93
93
|
requirements:
|
|
94
94
|
- - ">="
|