carson 2.13.0 → 2.13.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 +0 -14
- data/MANUAL.md +21 -5
- data/README.md +13 -3
- data/RELEASE.md +17 -1
- data/SKILL.md +0 -1
- data/VERSION +1 -1
- data/lib/carson/config.rb +1 -26
- data/lib/carson/runtime/audit.rb +0 -246
- data/lib/carson/runtime/local.rb +5 -1
- data/lib/carson/runtime/setup.rb +50 -33
- 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: d7fde15079ed380bb8c6e4d476fb8157dd7980ea6a250e06706188a645c1d160
|
|
4
|
+
data.tar.gz: 85a76cdb10a52573ba463a884079178e2b69befe21b8b377bb425f620057bd65
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 910cfd8d4ee3095b3ea559ba66bcd372d764043d02131f38d2d75071542aa04e1ea7cf5bdf10ab1aabd792f8ff21762ea31063720c23c719cc361d4313ea38fd
|
|
7
|
+
data.tar.gz: 236417f25de6aba2640a3ec992c5f756851b931f961f213408f57533533403f20d011cf4169cb8384ed980d58100456e49ea9209ed08fc2bb87a2d9729fcd9e4
|
data/API.md
CHANGED
|
@@ -98,8 +98,6 @@ Environment overrides:
|
|
|
98
98
|
- `CARSON_REVIEW_SWEEP_STATES`
|
|
99
99
|
- `CARSON_WORKFLOW_STYLE`
|
|
100
100
|
- `CARSON_RUBY_INDENTATION`
|
|
101
|
-
- `CARSON_LINT_COMMAND`
|
|
102
|
-
- `CARSON_LINT_ENFORCEMENT`
|
|
103
101
|
- `CARSON_LINT_POLICY_SOURCE`
|
|
104
102
|
|
|
105
103
|
`lint` schema:
|
|
@@ -107,29 +105,17 @@ Environment overrides:
|
|
|
107
105
|
```json
|
|
108
106
|
{
|
|
109
107
|
"lint": {
|
|
110
|
-
"command": "make lint",
|
|
111
|
-
"enforcement": "strict",
|
|
112
108
|
"policy_source": "wanghailei/lint.git"
|
|
113
109
|
}
|
|
114
110
|
}
|
|
115
111
|
```
|
|
116
112
|
|
|
117
113
|
`lint` semantics:
|
|
118
|
-
- `command`: string or array — local lint command executed during `carson audit`. If `null` (default), local lint is skipped.
|
|
119
|
-
- `enforcement`: `"strict"` (default) blocks on lint failure; `"advisory"` warns but does not block.
|
|
120
114
|
- `policy_source`: default source for `carson lint policy` when `--source` is not specified.
|
|
121
115
|
|
|
122
116
|
Environment overrides:
|
|
123
|
-
- `CARSON_LINT_COMMAND` — overrides `lint.command`.
|
|
124
|
-
- `CARSON_LINT_ENFORCEMENT` — overrides `lint.enforcement`.
|
|
125
117
|
- `CARSON_LINT_POLICY_SOURCE` — overrides `lint.policy_source`.
|
|
126
118
|
|
|
127
|
-
Lint target file source precedence in `carson audit`:
|
|
128
|
-
- staged files for local commit-time execution.
|
|
129
|
-
- PR changed files in GitHub `pull_request` events.
|
|
130
|
-
- full repository tracked files in GitHub non-PR events.
|
|
131
|
-
- local working-tree changes as fallback.
|
|
132
|
-
|
|
133
119
|
Private-source clone token for `carson lint policy`:
|
|
134
120
|
- `CARSON_READ_TOKEN` (used when `--source` points to a private GitHub repository).
|
|
135
121
|
|
data/MANUAL.md
CHANGED
|
@@ -27,7 +27,7 @@ carson version
|
|
|
27
27
|
|
|
28
28
|
### Step 1: Prepare your lint policy
|
|
29
29
|
|
|
30
|
-
Carson distributes lint configuration files from a central policy source — a directory or git repository you control — into each governed repo's `.github/linters/` directory, where MegaLinter auto-discovers them.
|
|
30
|
+
Carson distributes lint configuration files from a central policy source — a directory or git repository you control — into each governed repo's `.github/linters/` directory, where MegaLinter auto-discovers them in CI.
|
|
31
31
|
|
|
32
32
|
```bash
|
|
33
33
|
carson lint policy --source /path/to/your-policy-repo
|
|
@@ -35,6 +35,8 @@ carson lint policy --source /path/to/your-policy-repo
|
|
|
35
35
|
|
|
36
36
|
After this command, `.github/linters/` contains your lint configs (`.rubocop.yml`, `biome.json`, `ruff.toml`, etc.). MegaLinter uses these in CI. Every governed repository gets the same rules — this is how Carson keeps lint consistent across languages.
|
|
37
37
|
|
|
38
|
+
Carson distributes policy; MegaLinter enforces it. Carson does not run linters itself — that responsibility belongs to MegaLinter in CI and to the developer's own local tooling.
|
|
39
|
+
|
|
38
40
|
Options:
|
|
39
41
|
- `--source <path-or-git-url>` — where to read policy files from (required).
|
|
40
42
|
- `--ref <git-ref>` — branch or tag when `--source` is a git URL.
|
|
@@ -97,6 +99,17 @@ Notes:
|
|
|
97
99
|
- `CARSON_READ_TOKEN` must have read access to your policy source repository so CI can run `carson lint policy`.
|
|
98
100
|
- The reusable workflow installs a pinned RuboCop gem before `carson audit`; mirror the same pin in host governance workflows for deterministic checks.
|
|
99
101
|
|
|
102
|
+
### MegaLinter in CI
|
|
103
|
+
|
|
104
|
+
Carson manages a MegaLinter workflow template (`carson-lint.yml`) that is applied to governed repositories via `carson template apply`. MegaLinter auto-discovers lint configs from `.github/linters/` — the same directory populated by `carson lint policy`. This means:
|
|
105
|
+
|
|
106
|
+
- **Policy source** (your central lint repo) defines the rules.
|
|
107
|
+
- **`carson lint policy`** distributes the rules into each repo.
|
|
108
|
+
- **MegaLinter** (in GitHub Actions) enforces the rules.
|
|
109
|
+
- **`carson govern`** reads CI check status (including MegaLinter results) and acts on it.
|
|
110
|
+
|
|
111
|
+
Carson's `audit` gates on CI check status reported by GitHub — it does not duplicate MegaLinter's work locally. If MegaLinter fails in CI, `carson govern` classifies the PR accordingly and can dispatch a coding agent to fix the issues.
|
|
112
|
+
|
|
100
113
|
## Daily Operations
|
|
101
114
|
|
|
102
115
|
**Start of work:**
|
|
@@ -146,6 +159,12 @@ The loop is built-in and cross-platform — no cron, launchd, or Task Scheduler
|
|
|
146
159
|
|
|
147
160
|
Each cycle runs independently: if one cycle fails (network error, GitHub API timeout), the error is logged and the next cycle proceeds normally. Press `Ctrl-C` to stop — Carson exits cleanly with a cycle count summary.
|
|
148
161
|
|
|
162
|
+
### Govern and Coding Agents
|
|
163
|
+
|
|
164
|
+
`carson govern` dispatches coding agents (Codex or Claude) when a PR has failing CI checks. The agent receives the failure context and attempts to fix the issues in a follow-up commit. If the agent succeeds, the PR re-enters the governance pipeline. If it fails or times out, the PR is escalated for human attention.
|
|
165
|
+
|
|
166
|
+
The agent provider is configurable via `govern.agent.provider` (`auto`, `codex`, or `claude`). In `auto` mode, Carson selects the first available provider.
|
|
167
|
+
|
|
149
168
|
## Merge Method and Linear History
|
|
150
169
|
|
|
151
170
|
Carson's `govern.merge.method` controls how `carson govern` merges ready PRs. The options are `squash`, `merge`, and `rebase` (default: `squash`). Set this in `~/.carson/config.json`:
|
|
@@ -181,7 +200,7 @@ Carson's `govern.merge.method` controls how `carson govern` merges ready PRs. Th
|
|
|
181
200
|
These define what Carson *is*. They are not configurable.
|
|
182
201
|
|
|
183
202
|
- **Outsider boundary** — Carson never places its own artefacts inside a governed repository.
|
|
184
|
-
- **Centralised lint** — one policy source distributed into each repo's `.github/linters/`, zero per-repo drift.
|
|
203
|
+
- **Centralised lint** — one policy source distributed into each repo's `.github/linters/`, zero per-repo drift. MegaLinter enforces it in CI.
|
|
185
204
|
- **Active review** — undisposed reviewer findings block merge; feedback must be acknowledged.
|
|
186
205
|
- **Self-diagnosing output** — every message names the cause and the fix.
|
|
187
206
|
- **Transparent governance** — Carson prepares everything for merge but never makes decisions without telling you.
|
|
@@ -332,9 +351,6 @@ carson template apply
|
|
|
332
351
|
carson template check
|
|
333
352
|
```
|
|
334
353
|
|
|
335
|
-
**Audit blocks on lint failure**
|
|
336
|
-
- If `lint.command` is configured and fails, audit blocks (in `strict` mode) or warns (in `advisory` mode). Fix the lint issues and re-run `carson audit`.
|
|
337
|
-
|
|
338
354
|
**Hook version mismatch after upgrade**
|
|
339
355
|
- Run `carson refresh` to re-apply hooks and templates for the new Carson version.
|
|
340
356
|
- Run `carson refresh --all` to refresh all governed repositories at once.
|
data/README.md
CHANGED
|
@@ -12,12 +12,12 @@ If you govern more than a handful of repositories, you know the pattern: lint co
|
|
|
12
12
|
|
|
13
13
|
Carson is an autonomous governance runtime that lives on your workstation and in CI, never inside the repositories it governs. It operates at two levels:
|
|
14
14
|
|
|
15
|
-
**Per-commit governance** — Carson
|
|
15
|
+
**Per-commit governance** — Carson gates merges on unresolved review comments, synchronises templates, and keeps your local branches clean. Every commit triggers `carson audit` through managed hooks; the same checks run in GitHub Actions. Lint execution is handled entirely by MegaLinter in CI and by the developer's own local tooling — Carson distributes the shared lint configuration but does not run linters itself.
|
|
16
16
|
|
|
17
17
|
**Portfolio-level autonomy** — `carson govern` is a scheduled triage loop that scans all your repositories, classifies every open PR, and acts: merge what's ready, dispatch coding agents (Codex or Claude) to fix what's failing, and escalate what needs human judgement. One command, all your projects, unmanned.
|
|
18
18
|
|
|
19
19
|
```
|
|
20
|
-
|
|
20
|
+
┌──────────────────────────────────────────────┐
|
|
21
21
|
│ Your workstation │
|
|
22
22
|
│ │
|
|
23
23
|
│ ~/.carson/ Carson config │
|
|
@@ -39,12 +39,22 @@ Carson is an autonomous governance runtime that lives on your workstation and in
|
|
|
39
39
|
|
|
40
40
|
This separation is Carson's defining trait — the **outsider boundary**: no Carson scripts, config files, or governance payloads are ever placed inside a governed repository.
|
|
41
41
|
|
|
42
|
+
### The Governance Loop
|
|
43
|
+
|
|
44
|
+
Carson orchestrates a closed governance loop across three layers:
|
|
45
|
+
|
|
46
|
+
1. **Policy distribution** — `carson lint policy` distributes shared lint configs from a central source into each repo's `.github/linters/`. This is the single source of truth for lint rules across all governed repositories.
|
|
47
|
+
2. **CI enforcement** — MegaLinter runs in GitHub Actions (via the managed `carson-lint.yml` workflow) and auto-discovers configs from `.github/linters/`. Carson does not run linters — MegaLinter does. Carson's `audit` gates on CI check status reported by GitHub.
|
|
48
|
+
3. **Autonomous triage** — `carson govern` reads CI status (including MegaLinter results), review disposition, and audit health for every open PR. Ready PRs are merged. Failing PRs get a coding agent (Codex or Claude) dispatched to fix them. Stuck PRs are escalated.
|
|
49
|
+
|
|
50
|
+
Carson's role is governance orchestration — distributing policy, gating on results, and dispatching action. The actual lint execution, CI runs, and code fixes are delegated to specialised tools: MegaLinter for linting, GitHub Actions for CI, and coding agents for remediation.
|
|
51
|
+
|
|
42
52
|
## Opinions
|
|
43
53
|
|
|
44
54
|
Carson is opinionated about governance. These are non-negotiable principles, not configurable defaults:
|
|
45
55
|
|
|
46
56
|
- **Outsider boundary** — Carson lives outside your repo, never inside. No Carson-owned artefacts in your repository. Offboarding leaves no trace.
|
|
47
|
-
- **Centralised lint** — lint policy distributed from a central source into each repo's `.github/linters/`. One source of truth, zero drift.
|
|
57
|
+
- **Centralised lint** — lint policy distributed from a central source into each repo's `.github/linters/`. One source of truth, zero drift. MegaLinter enforces it in CI.
|
|
48
58
|
- **Active review** — undisposed reviewer findings block merge. Feedback must be acknowledged, not buried.
|
|
49
59
|
- **Self-diagnosing output** — every message names the cause and the fix. If you need to debug Carson's output, the output failed.
|
|
50
60
|
- **Transparent governance** — Carson prepares everything for merge but never oversteps. It does not make decisions for you without telling you.
|
data/RELEASE.md
CHANGED
|
@@ -5,11 +5,27 @@ 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.13.
|
|
8
|
+
## 2.13.1 — Guided Governance Registration
|
|
9
|
+
|
|
10
|
+
### What changed
|
|
11
|
+
|
|
12
|
+
- After `carson onboard`, Carson now prompts to register the repo for portfolio governance (`govern.repos`). Accept to include it in `carson refresh --all` and `carson govern`; decline to skip.
|
|
13
|
+
- Improved `refresh --all` guidance when no repos are configured — now directs users to `carson onboard`.
|
|
14
|
+
|
|
15
|
+
## 2.13.0 — Refresh All + Strip Local Lint Execution
|
|
9
16
|
|
|
10
17
|
### What changed
|
|
11
18
|
|
|
12
19
|
- **`carson refresh --all`** refreshes every governed repository in a single command. Iterates `govern.repos`, runs hooks + templates + audit on each, prints a per-repo summary line, and returns non-zero if any repo fails. Verbose mode streams full diagnostics per repo.
|
|
20
|
+
- **Removed `lint.command` and `lint.enforcement` config keys.** Local lint execution during `carson audit` has been removed. MegaLinter runs in CI and `carson govern` gates on CI check status — local lint execution was redundant. Carson now focuses on what makes it unique: **policy distribution** via `carson lint policy`. The `lint.policy_source` config key and `carson lint policy --source` command are unchanged.
|
|
21
|
+
- **Removed `CARSON_LINT_COMMAND` and `CARSON_LINT_ENFORCEMENT` environment overrides.**
|
|
22
|
+
- **Removed lint command and enforcement prompts from `carson setup`.**
|
|
23
|
+
|
|
24
|
+
### Migration
|
|
25
|
+
|
|
26
|
+
1. Remove `lint.command` and `lint.enforcement` from `~/.carson/config.json` if present — they are now ignored.
|
|
27
|
+
2. Remove `CARSON_LINT_COMMAND` and `CARSON_LINT_ENFORCEMENT` from any CI or shell configuration.
|
|
28
|
+
3. If you relied on local lint during audit, run your lint tool directly (e.g. `make lint`, `trunk check`) or let MegaLinter handle it in CI.
|
|
13
29
|
|
|
14
30
|
## 2.12.0 — Language-Agnostic Lint Policy Distribution + MegaLinter
|
|
15
31
|
|
data/SKILL.md
CHANGED
|
@@ -33,7 +33,6 @@ When you see exit 2, do NOT bypass it. Read the output, fix the root cause, and
|
|
|
33
33
|
Carson audit output is structured as labelled key-value lines prefixed with ⧓. Key sections:
|
|
34
34
|
|
|
35
35
|
- **Working Tree** — staged/unstaged status.
|
|
36
|
-
- **Local Lint Quality** — lint command result. `lint_command_status: ok` means clean.
|
|
37
36
|
- **Main Sync Status** — whether local main matches remote. If ahead, reset drift before committing.
|
|
38
37
|
- **Scope Integrity Guard** — checks that commits stay within a single business intent and scope group.
|
|
39
38
|
- **Audit Result** — final verdict: `status: ok` (clean), `status: attention` (advisory, not blocking), `status: block` (must fix).
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
2.13.
|
|
1
|
+
2.13.1
|
data/lib/carson/config.rb
CHANGED
|
@@ -9,7 +9,7 @@ module Carson
|
|
|
9
9
|
attr_accessor :git_remote
|
|
10
10
|
attr_reader :main_branch, :protected_branches, :hooks_base_path, :required_hooks,
|
|
11
11
|
:path_groups, :template_managed_files,
|
|
12
|
-
:
|
|
12
|
+
:lint_policy_source,
|
|
13
13
|
:review_wait_seconds, :review_poll_seconds, :review_max_polls, :review_sweep_window_days,
|
|
14
14
|
:review_sweep_states, :review_disposition_prefix, :review_risk_keywords,
|
|
15
15
|
:review_tracking_issue_title, :review_tracking_issue_label, :review_bot_usernames,
|
|
@@ -51,8 +51,6 @@ module Carson
|
|
|
51
51
|
"managed_files" => [ ".github/carson-instructions.md", ".github/copilot-instructions.md", ".github/CLAUDE.md", ".github/AGENTS.md", ".github/pull_request_template.md", ".github/workflows/carson-lint.yml" ]
|
|
52
52
|
},
|
|
53
53
|
"lint" => {
|
|
54
|
-
"command" => nil,
|
|
55
|
-
"enforcement" => "strict",
|
|
56
54
|
"policy_source" => "wanghailei/lint.git"
|
|
57
55
|
},
|
|
58
56
|
"workflow" => {
|
|
@@ -168,10 +166,6 @@ module Carson
|
|
|
168
166
|
advisory_names = env_string_array( key: "CARSON_AUDIT_ADVISORY_CHECK_NAMES" )
|
|
169
167
|
audit[ "advisory_check_names" ] = advisory_names unless advisory_names.empty?
|
|
170
168
|
lint = fetch_hash_section( data: copy, key: "lint" )
|
|
171
|
-
lint_command_env = ENV.fetch( "CARSON_LINT_COMMAND", "" ).to_s.strip
|
|
172
|
-
lint[ "command" ] = lint_command_env unless lint_command_env.empty?
|
|
173
|
-
lint_enforcement_env = ENV.fetch( "CARSON_LINT_ENFORCEMENT", "" ).to_s.strip
|
|
174
|
-
lint[ "enforcement" ] = lint_enforcement_env unless lint_enforcement_env.empty?
|
|
175
169
|
lint_policy_source_env = ENV.fetch( "CARSON_LINT_POLICY_SOURCE", "" ).to_s.strip
|
|
176
170
|
lint[ "policy_source" ] = lint_policy_source_env unless lint_policy_source_env.empty?
|
|
177
171
|
style = fetch_hash_section( data: copy, key: "style" )
|
|
@@ -223,8 +217,6 @@ module Carson
|
|
|
223
217
|
|
|
224
218
|
@template_managed_files = fetch_string_array( hash: fetch_hash( hash: data, key: "template" ), key: "managed_files" )
|
|
225
219
|
lint_hash = fetch_hash( hash: data, key: "lint" )
|
|
226
|
-
@lint_command = normalize_lint_command_setting( value: lint_hash[ "command" ] )
|
|
227
|
-
@lint_enforcement = normalize_lint_enforcement( value: lint_hash.fetch( "enforcement", "strict" ) )
|
|
228
220
|
@lint_policy_source = lint_hash.fetch( "policy_source", "" ).to_s.strip
|
|
229
221
|
|
|
230
222
|
workflow_hash = fetch_hash( hash: data, key: "workflow" )
|
|
@@ -338,23 +330,6 @@ module Carson
|
|
|
338
330
|
patterns
|
|
339
331
|
end
|
|
340
332
|
|
|
341
|
-
def normalize_lint_command_setting( value: )
|
|
342
|
-
return nil if value.nil?
|
|
343
|
-
return value.to_s.strip if value.is_a?( String )
|
|
344
|
-
if value.is_a?( Array )
|
|
345
|
-
parts = value.map { |entry| entry.to_s.strip }.reject( &:empty? )
|
|
346
|
-
raise ConfigError, "lint.command array must contain at least one argument" if parts.empty?
|
|
347
|
-
return parts
|
|
348
|
-
end
|
|
349
|
-
raise ConfigError, "lint.command must be a string, array, or null"
|
|
350
|
-
end
|
|
351
|
-
|
|
352
|
-
def normalize_lint_enforcement( value: )
|
|
353
|
-
text = value.to_s.strip.downcase
|
|
354
|
-
raise ConfigError, "lint.enforcement must be one of strict, advisory" unless [ "strict", "advisory" ].include?( text )
|
|
355
|
-
text
|
|
356
|
-
end
|
|
357
|
-
|
|
358
333
|
def fetch_optional_boolean( hash:, key:, default:, key_path: nil )
|
|
359
334
|
value = hash.fetch( key, default )
|
|
360
335
|
return true if value == true
|
data/lib/carson/runtime/audit.rb
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
require "cgi"
|
|
2
|
-
require "open3"
|
|
3
2
|
|
|
4
3
|
module Carson
|
|
5
4
|
class Runtime
|
|
@@ -24,17 +23,6 @@ module Carson
|
|
|
24
23
|
audit_concise_problems << "Hooks: mismatch — run carson prepare."
|
|
25
24
|
end
|
|
26
25
|
puts_verbose ""
|
|
27
|
-
puts_verbose "[Local Lint Quality]"
|
|
28
|
-
local_lint_quality = local_lint_quality_report
|
|
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
26
|
puts_verbose "[Main Sync Status]"
|
|
39
27
|
ahead_count, behind_count, main_error = main_sync_counts
|
|
40
28
|
if main_error
|
|
@@ -71,7 +59,6 @@ module Carson
|
|
|
71
59
|
audit_state = "attention" if audit_state == "ok" && scope_guard.fetch( :status ) == "attention"
|
|
72
60
|
write_and_print_pr_monitor_report(
|
|
73
61
|
report: monitor_report.merge(
|
|
74
|
-
local_lint_quality: local_lint_quality,
|
|
75
62
|
default_branch_baseline: default_branch_baseline,
|
|
76
63
|
audit_status: audit_state
|
|
77
64
|
)
|
|
@@ -162,217 +149,6 @@ module Carson
|
|
|
162
149
|
report
|
|
163
150
|
end
|
|
164
151
|
|
|
165
|
-
# Enforces configured lint policy before governance passes.
|
|
166
|
-
# Runs lint.command and gates on exit code. Skips when lint.command is not set.
|
|
167
|
-
def local_lint_quality_report
|
|
168
|
-
unless config.lint_command
|
|
169
|
-
report = {
|
|
170
|
-
status: "ok",
|
|
171
|
-
skip_reason: "lint.command not configured",
|
|
172
|
-
target_source: "none",
|
|
173
|
-
target_files_count: 0,
|
|
174
|
-
blocking_languages: 0,
|
|
175
|
-
languages: []
|
|
176
|
-
}
|
|
177
|
-
puts_verbose "lint: SKIP (lint.command not configured)"
|
|
178
|
-
return report
|
|
179
|
-
end
|
|
180
|
-
|
|
181
|
-
lint_command_report
|
|
182
|
-
rescue StandardError => e
|
|
183
|
-
report = {
|
|
184
|
-
status: "block",
|
|
185
|
-
skip_reason: e.message,
|
|
186
|
-
target_source: "unknown",
|
|
187
|
-
target_files_count: 0,
|
|
188
|
-
blocking_languages: 0,
|
|
189
|
-
languages: []
|
|
190
|
-
}
|
|
191
|
-
puts_line "BLOCK: local lint quality check failed (#{e.message})."
|
|
192
|
-
report
|
|
193
|
-
end
|
|
194
|
-
|
|
195
|
-
# Runs the lint.command and returns a structured report.
|
|
196
|
-
def lint_command_report
|
|
197
|
-
target_files, target_source = lint_target_files
|
|
198
|
-
advisory = config.lint_enforcement == "advisory"
|
|
199
|
-
command_value = config.lint_command
|
|
200
|
-
command_string = command_value.is_a?( Array ) ? command_value.join( " " ) : command_value.to_s
|
|
201
|
-
|
|
202
|
-
report = {
|
|
203
|
-
status: "ok",
|
|
204
|
-
skip_reason: nil,
|
|
205
|
-
target_source: target_source,
|
|
206
|
-
target_files_count: target_files.count,
|
|
207
|
-
blocking_languages: 0,
|
|
208
|
-
languages: []
|
|
209
|
-
}
|
|
210
|
-
puts_verbose "lint_target_source: #{target_source}"
|
|
211
|
-
puts_verbose "lint_target_files_total: #{target_files.count}"
|
|
212
|
-
puts_verbose "lint_command: #{command_string}"
|
|
213
|
-
puts_verbose "lint_enforcement: #{config.lint_enforcement}"
|
|
214
|
-
|
|
215
|
-
args = command_string.split( /\s+/ )
|
|
216
|
-
command_name = args.first.to_s.strip
|
|
217
|
-
unless command_available_for_lint?( command_name: command_name )
|
|
218
|
-
language_report = {
|
|
219
|
-
language: "lint.command",
|
|
220
|
-
enabled: true,
|
|
221
|
-
status: "block",
|
|
222
|
-
reason: "command not available: #{command_name}",
|
|
223
|
-
file_count: target_files.count,
|
|
224
|
-
files: target_files,
|
|
225
|
-
command: args,
|
|
226
|
-
config_files: [],
|
|
227
|
-
exit_code: EXIT_BLOCK
|
|
228
|
-
}
|
|
229
|
-
report[ :languages ] << language_report
|
|
230
|
-
report[ :status ] = advisory ? "ok" : "block"
|
|
231
|
-
report[ :blocking_languages ] = advisory ? 0 : 1
|
|
232
|
-
puts_verbose "lint_command_status: #{language_report.fetch( :status )}"
|
|
233
|
-
puts_line "WARN: lint command not available: #{command_name}" if advisory
|
|
234
|
-
return report
|
|
235
|
-
end
|
|
236
|
-
|
|
237
|
-
stdout_text, stderr_text, success, exit_code = local_command( *args )
|
|
238
|
-
language_report = {
|
|
239
|
-
language: "lint.command",
|
|
240
|
-
enabled: true,
|
|
241
|
-
status: success ? "ok" : "block",
|
|
242
|
-
reason: success ? nil : summarise_command_output(
|
|
243
|
-
stdout_text: stdout_text,
|
|
244
|
-
stderr_text: stderr_text,
|
|
245
|
-
fallback: "lint command failed"
|
|
246
|
-
),
|
|
247
|
-
file_count: target_files.count,
|
|
248
|
-
files: target_files,
|
|
249
|
-
command: args,
|
|
250
|
-
config_files: [],
|
|
251
|
-
exit_code: exit_code
|
|
252
|
-
}
|
|
253
|
-
report[ :languages ] << language_report
|
|
254
|
-
|
|
255
|
-
unless success
|
|
256
|
-
if advisory
|
|
257
|
-
report[ :status ] = "ok"
|
|
258
|
-
puts_verbose "lint_command_status: advisory_warn (exit #{exit_code})"
|
|
259
|
-
puts_line "WARN: lint command failed (exit #{exit_code}) — advisory mode, not blocking."
|
|
260
|
-
else
|
|
261
|
-
report[ :status ] = "block"
|
|
262
|
-
report[ :blocking_languages ] = 1
|
|
263
|
-
puts_verbose "lint_command_status: block (exit #{exit_code})"
|
|
264
|
-
end
|
|
265
|
-
else
|
|
266
|
-
puts_verbose "lint_command_status: ok"
|
|
267
|
-
end
|
|
268
|
-
|
|
269
|
-
report
|
|
270
|
-
end
|
|
271
|
-
|
|
272
|
-
# File selection precedence:
|
|
273
|
-
# 1) staged files for local commit-time execution
|
|
274
|
-
# 2) PR changed files in GitHub pull_request events
|
|
275
|
-
# 3) full repository tracked files in GitHub non-PR events
|
|
276
|
-
# 4) local working-tree changed files as fallback
|
|
277
|
-
def lint_target_files
|
|
278
|
-
staged = existing_repo_files( paths: staged_files )
|
|
279
|
-
return [ staged, "staged" ] unless staged.empty?
|
|
280
|
-
|
|
281
|
-
if github_pull_request_event?
|
|
282
|
-
files = lint_target_files_for_pull_request
|
|
283
|
-
return [ files, "github_pull_request" ] unless files.nil?
|
|
284
|
-
puts_verbose "WARN: unable to resolve pull request changed files; falling back to full repository files."
|
|
285
|
-
end
|
|
286
|
-
|
|
287
|
-
if github_actions_environment?
|
|
288
|
-
return [ lint_target_files_for_non_pr_ci, "github_full_repository" ]
|
|
289
|
-
end
|
|
290
|
-
|
|
291
|
-
[ existing_repo_files( paths: changed_files ), "working_tree" ]
|
|
292
|
-
end
|
|
293
|
-
|
|
294
|
-
def lint_target_files_for_pull_request
|
|
295
|
-
base_ref = ENV.fetch( "GITHUB_BASE_REF", "" ).to_s.strip
|
|
296
|
-
return nil if base_ref.empty?
|
|
297
|
-
|
|
298
|
-
remote_name = config.git_remote
|
|
299
|
-
unless git_remote_exists?( remote_name: remote_name )
|
|
300
|
-
remote_name = "origin" if git_remote_exists?( remote_name: "origin" )
|
|
301
|
-
end
|
|
302
|
-
|
|
303
|
-
_, _, fetch_success, = git_run( "fetch", "--no-tags", "--depth", "1", remote_name, base_ref )
|
|
304
|
-
return nil unless fetch_success
|
|
305
|
-
|
|
306
|
-
base = "#{remote_name}/#{base_ref}"
|
|
307
|
-
stdout_text, _, success, = git_run(
|
|
308
|
-
"diff", "--name-only", "--diff-filter=ACMRTUXB", "#{base}...HEAD"
|
|
309
|
-
)
|
|
310
|
-
return nil unless success
|
|
311
|
-
|
|
312
|
-
paths = stdout_text.lines.map { |line| line.to_s.strip }.reject( &:empty? )
|
|
313
|
-
existing_repo_files( paths: paths )
|
|
314
|
-
end
|
|
315
|
-
|
|
316
|
-
def lint_target_files_for_non_pr_ci
|
|
317
|
-
stdout_text = git_capture!( "ls-files" )
|
|
318
|
-
paths = stdout_text.lines.map { |line| line.to_s.strip }.reject( &:empty? )
|
|
319
|
-
existing_repo_files( paths: paths )
|
|
320
|
-
end
|
|
321
|
-
|
|
322
|
-
def github_actions_environment?
|
|
323
|
-
ENV.fetch( "GITHUB_ACTIONS", "" ).to_s.strip.casecmp( "true" ).zero?
|
|
324
|
-
end
|
|
325
|
-
|
|
326
|
-
def github_pull_request_event?
|
|
327
|
-
return false unless github_actions_environment?
|
|
328
|
-
|
|
329
|
-
event_name = ENV.fetch( "GITHUB_EVENT_NAME", "" ).to_s.strip
|
|
330
|
-
[ "pull_request", "pull_request_target" ].include?( event_name )
|
|
331
|
-
end
|
|
332
|
-
|
|
333
|
-
def existing_repo_files( paths: )
|
|
334
|
-
Array( paths ).map do |relative|
|
|
335
|
-
next if relative.to_s.strip.empty?
|
|
336
|
-
absolute = resolve_repo_path!( relative_path: relative, label: "lint target file #{relative}" )
|
|
337
|
-
next unless File.file?( absolute )
|
|
338
|
-
relative
|
|
339
|
-
end.compact.uniq
|
|
340
|
-
end
|
|
341
|
-
|
|
342
|
-
def command_available_for_lint?( command_name: )
|
|
343
|
-
return false if command_name.to_s.strip.empty?
|
|
344
|
-
|
|
345
|
-
if command_name.include?( "/" )
|
|
346
|
-
path = if command_name.start_with?( "~" )
|
|
347
|
-
File.expand_path( command_name )
|
|
348
|
-
elsif command_name.start_with?( "/" )
|
|
349
|
-
command_name
|
|
350
|
-
else
|
|
351
|
-
File.expand_path( command_name, repo_root )
|
|
352
|
-
end
|
|
353
|
-
return File.executable?( path )
|
|
354
|
-
end
|
|
355
|
-
path_entries = ENV.fetch( "PATH", "" ).split( File::PATH_SEPARATOR )
|
|
356
|
-
path_entries.any? do |entry|
|
|
357
|
-
next false if entry.to_s.strip.empty?
|
|
358
|
-
File.executable?( File.join( entry, command_name ) )
|
|
359
|
-
end
|
|
360
|
-
end
|
|
361
|
-
|
|
362
|
-
# Local command runner for repository-context tools used by audit lint checks.
|
|
363
|
-
def local_command( *args )
|
|
364
|
-
stdout_text, stderr_text, status = Open3.capture3( *args, chdir: repo_root )
|
|
365
|
-
[ stdout_text, stderr_text, status.success?, status.exitstatus ]
|
|
366
|
-
end
|
|
367
|
-
|
|
368
|
-
# Compacts command output to one-line diagnostics for audit logs and JSON report payloads.
|
|
369
|
-
def summarise_command_output( stdout_text:, stderr_text:, fallback: )
|
|
370
|
-
combined = [ stderr_text.to_s, stdout_text.to_s ].join( "\n" )
|
|
371
|
-
lines = combined.lines.map { |line| line.to_s.strip }.reject( &:empty? )
|
|
372
|
-
return fallback if lines.empty?
|
|
373
|
-
lines.first( 12 ).join( " | " )
|
|
374
|
-
end
|
|
375
|
-
|
|
376
152
|
# Evaluates default-branch CI health so stale workflow drift blocks before merge.
|
|
377
153
|
def default_branch_ci_baseline_report
|
|
378
154
|
report = {
|
|
@@ -630,28 +406,6 @@ module Carson
|
|
|
630
406
|
checks.fetch( :pending ).each { |entry| lines << "- #{entry.fetch( :workflow )} / #{entry.fetch( :name )} (#{entry.fetch( :state )}) #{entry.fetch( :link )}".strip }
|
|
631
407
|
end
|
|
632
408
|
lines << ""
|
|
633
|
-
lines << "## Local Lint Quality"
|
|
634
|
-
lint_quality = report[ :local_lint_quality ]
|
|
635
|
-
if lint_quality.nil?
|
|
636
|
-
lines << "- not available"
|
|
637
|
-
else
|
|
638
|
-
lines << "- Status: #{lint_quality.fetch( :status )}"
|
|
639
|
-
lines << "- Skip reason: #{lint_quality.fetch( :skip_reason )}" unless lint_quality.fetch( :skip_reason ).nil?
|
|
640
|
-
lines << "- Target source: #{lint_quality.fetch( :target_source )}"
|
|
641
|
-
lines << "- Target files: #{lint_quality.fetch( :target_files_count )}"
|
|
642
|
-
lines << "- Blocking languages: #{lint_quality.fetch( :blocking_languages )}"
|
|
643
|
-
lines << ""
|
|
644
|
-
lines << "### Language Results"
|
|
645
|
-
if lint_quality.fetch( :languages ).empty?
|
|
646
|
-
lines << "- none"
|
|
647
|
-
else
|
|
648
|
-
lint_quality.fetch( :languages ).each do |entry|
|
|
649
|
-
lines << "- #{entry.fetch( :language )}: status=#{entry.fetch( :status )} files=#{entry.fetch( :file_count )} exit=#{entry.fetch( :exit_code )}"
|
|
650
|
-
lines << " reason: #{entry.fetch( :reason )}" unless entry.fetch( :reason ).nil?
|
|
651
|
-
end
|
|
652
|
-
end
|
|
653
|
-
end
|
|
654
|
-
lines << ""
|
|
655
409
|
lines << "## Default Branch CI Baseline"
|
|
656
410
|
baseline = report[ :default_branch_baseline ]
|
|
657
411
|
if baseline.nil?
|
data/lib/carson/runtime/local.rb
CHANGED
|
@@ -251,7 +251,8 @@ module Carson
|
|
|
251
251
|
def refresh_all!
|
|
252
252
|
repos = config.govern_repos
|
|
253
253
|
if repos.empty?
|
|
254
|
-
puts_line "
|
|
254
|
+
puts_line "No governed repositories configured."
|
|
255
|
+
puts_line " Run carson onboard in each repo to register."
|
|
255
256
|
return EXIT_ERROR
|
|
256
257
|
end
|
|
257
258
|
|
|
@@ -813,6 +814,9 @@ module Carson
|
|
|
813
814
|
puts_line ""
|
|
814
815
|
puts_line "Carson is ready. Workflow: #{config.workflow_style}"
|
|
815
816
|
puts_line "Reconfigure anytime: carson setup"
|
|
817
|
+
|
|
818
|
+
prompt_govern_registration! if self.in.respond_to?( :tty? ) && self.in.tty?
|
|
819
|
+
|
|
816
820
|
audit_status
|
|
817
821
|
end
|
|
818
822
|
|
data/lib/carson/runtime/setup.rb
CHANGED
|
@@ -39,11 +39,6 @@ module Carson
|
|
|
39
39
|
merge_choice = prompt_merge_method
|
|
40
40
|
choices[ "govern.merge.method" ] = merge_choice unless merge_choice.nil?
|
|
41
41
|
|
|
42
|
-
lint_command_choice = prompt_lint_command
|
|
43
|
-
choices[ "lint.command" ] = lint_command_choice unless lint_command_choice.nil?
|
|
44
|
-
|
|
45
|
-
lint_enforcement_choice = prompt_lint_enforcement
|
|
46
|
-
choices[ "lint.enforcement" ] = lint_enforcement_choice unless lint_enforcement_choice.nil?
|
|
47
42
|
|
|
48
43
|
write_setup_config( choices: choices )
|
|
49
44
|
end
|
|
@@ -148,34 +143,6 @@ module Carson
|
|
|
148
143
|
prompt_choice( options: options, default: 0 )
|
|
149
144
|
end
|
|
150
145
|
|
|
151
|
-
def prompt_lint_command
|
|
152
|
-
puts_line ""
|
|
153
|
-
puts_line "Lint command"
|
|
154
|
-
options = [
|
|
155
|
-
{ label: "make lint", value: "make lint" },
|
|
156
|
-
{ label: "trunk check (Recommended)", value: "trunk check" },
|
|
157
|
-
{ label: "Other (enter command)", value: :other },
|
|
158
|
-
{ label: "Skip (no local lint)", value: nil }
|
|
159
|
-
]
|
|
160
|
-
choice = prompt_choice( options: options, default: 1 )
|
|
161
|
-
|
|
162
|
-
if choice == :other
|
|
163
|
-
prompt_custom_value( label: "Lint command" )
|
|
164
|
-
else
|
|
165
|
-
choice
|
|
166
|
-
end
|
|
167
|
-
end
|
|
168
|
-
|
|
169
|
-
def prompt_lint_enforcement
|
|
170
|
-
puts_line ""
|
|
171
|
-
puts_line "Lint enforcement"
|
|
172
|
-
options = [
|
|
173
|
-
{ label: "strict — block on lint failure (default)", value: "strict" },
|
|
174
|
-
{ label: "advisory — warn but don't block", value: "advisory" }
|
|
175
|
-
]
|
|
176
|
-
prompt_choice( options: options, default: 0 )
|
|
177
|
-
end
|
|
178
|
-
|
|
179
146
|
def prompt_choice( options:, default: )
|
|
180
147
|
options.each_with_index do |option, index|
|
|
181
148
|
puts_line " #{index + 1}) #{option.fetch( :label )}"
|
|
@@ -374,6 +341,56 @@ module Carson
|
|
|
374
341
|
path = Config.global_config_path( repo_root: repo_root )
|
|
375
342
|
!path.empty? && File.file?( path )
|
|
376
343
|
end
|
|
344
|
+
|
|
345
|
+
# After onboard succeeds, offer to register the repo for portfolio governance.
|
|
346
|
+
def prompt_govern_registration!
|
|
347
|
+
expanded = File.expand_path( repo_root )
|
|
348
|
+
if config.govern_repos.include?( expanded )
|
|
349
|
+
puts_verbose "govern_registration: already registered #{expanded}"
|
|
350
|
+
return
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
puts_line ""
|
|
354
|
+
puts_line "Portfolio governance"
|
|
355
|
+
puts_line " Register this repo so carson refresh --all and carson govern include it?"
|
|
356
|
+
accepted = prompt_yes_no( default: true )
|
|
357
|
+
if accepted
|
|
358
|
+
append_govern_repo!( repo_path: expanded )
|
|
359
|
+
puts_line "Registered."
|
|
360
|
+
else
|
|
361
|
+
puts_line "Skipped. Re-run carson onboard to register later."
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
# Reusable Y/n prompt following existing prompt_choice conventions.
|
|
366
|
+
def prompt_yes_no( default: true )
|
|
367
|
+
hint = default ? "Y/n" : "y/N"
|
|
368
|
+
out.print "#{BADGE} [#{hint}]: "
|
|
369
|
+
out.flush
|
|
370
|
+
raw = self.in.gets
|
|
371
|
+
return default if raw.nil?
|
|
372
|
+
|
|
373
|
+
input = raw.to_s.strip.downcase
|
|
374
|
+
return default if input.empty?
|
|
375
|
+
|
|
376
|
+
input.start_with?( "y" )
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
# Appends a repo path to govern.repos without replacing the array via deep_merge.
|
|
380
|
+
def append_govern_repo!( repo_path: )
|
|
381
|
+
config_path = Config.global_config_path( repo_root: repo_root )
|
|
382
|
+
return if config_path.empty?
|
|
383
|
+
|
|
384
|
+
existing_data = load_existing_config( path: config_path )
|
|
385
|
+
existing_data[ "govern" ] ||= {}
|
|
386
|
+
repos = Array( existing_data[ "govern" ][ "repos" ] )
|
|
387
|
+
repos << repo_path
|
|
388
|
+
existing_data[ "govern" ][ "repos" ] = repos.uniq
|
|
389
|
+
|
|
390
|
+
FileUtils.mkdir_p( File.dirname( config_path ) )
|
|
391
|
+
File.write( config_path, JSON.pretty_generate( existing_data ) )
|
|
392
|
+
reload_config_after_setup!
|
|
393
|
+
end
|
|
377
394
|
end
|
|
378
395
|
|
|
379
396
|
include Setup
|