carson 4.3.1 → 4.3.2
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/.github/workflows/carson_policy.yml +0 -4
- data/API.md +2 -3
- data/MANUAL.md +8 -8
- data/README.md +1 -1
- data/RELEASE.md +18 -0
- data/VERSION +1 -1
- data/config/hooks/pre-commit +2 -17
- data/lib/carson/adapters/agent.rb +1 -1
- data/lib/carson/config.rb +0 -10
- data/lib/carson/runtime/local/onboard.rb +9 -43
- data/lib/carson/runtime/receive.rb +1 -1
- data/lib/carson/runtime/recover.rb +3 -3
- data/lib/carson/runtime/review/sweep_support.rb +1 -1
- data/lib/carson/runtime.rb +2 -5
- data/lib/carson/warehouse/vault.rb +15 -5
- data/lib/cli.rb +4 -38
- metadata +1 -2
- data/lib/carson/runtime/audit.rb +0 -603
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cef1919871b9d457a8b1ad95b2faa127cbb1a740fa211bf48e1b18110141be5a
|
|
4
|
+
data.tar.gz: bd4f1107862848b8a60d5ef794f43127a6b8f14146ed781bfe72d841220f4190
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c4047e729026abc15be0601316a3521249cdc392c60b24b6ebf61bf5454746d48d649f16f2cda705e5a01ed2c9cce91ac2b407fd8d10ad670453fd2d0742d243
|
|
7
|
+
data.tar.gz: 9dee7343ec14dbf351fce53e0da6b406e52fe64bb71e736bb70ad23dc22c2e82013d34705d901d7b304d79b6cb40ef1cb8cc0c3412da9796ab6d853e81648156
|
data/API.md
CHANGED
|
@@ -21,7 +21,7 @@ Repo-scoped commands: `carson <repo> <command>` or `carson <command>` when CWD i
|
|
|
21
21
|
|---|---|
|
|
22
22
|
| `carson setup` | Interactive quiz to configure remote, main branch, workflow, and canonical lint-policy path. Writes `~/.carson/config.json`. |
|
|
23
23
|
| `carson onboard <repo_path>` | Apply one-command baseline setup for a target git repository. Auto-triggers `setup` on first run. Installs or refreshes Carson-managed global hooks. |
|
|
24
|
-
| `carson refresh` | Re-apply hooks
|
|
24
|
+
| `carson refresh` | Re-apply hooks and templates across all governed repos after upgrading Carson. Auto-propagates template updates to the remote via worktree (branch workflow: PR on `carson/template-sync`; trunk workflow: push to main). Skips repos with active worktrees or uncommitted changes. |
|
|
25
25
|
| `carson offboard <repo_path>` | Remove Carson-managed host artefacts, detach Carson hooks path, and deregister from `govern.repos`. |
|
|
26
26
|
| `carson list [--json]` | List all governed repositories. |
|
|
27
27
|
|
|
@@ -29,9 +29,8 @@ Repo-scoped commands: `carson <repo> <command>` or `carson <command>` when CWD i
|
|
|
29
29
|
|
|
30
30
|
| Command | Purpose |
|
|
31
31
|
|---|---|
|
|
32
|
-
| `carson audit` | Evaluate governance status and generate report output. |
|
|
33
32
|
| `carson deliver [--commit MESSAGE]` | Run Carson-owned branch delivery for the current checkout. Plain `deliver` transports existing commits only; `--commit` creates one all-dirty delivery commit first. Before push, Carson verifies the branch is fresh against the configured remote `main`; behind or unknown freshness blocks delivery without creating or refreshing a PR. If freshness is good, Carson pushes, creates or refreshes the PR, watches the delivery for a bounded settle window, merges when clear, syncs local `main`, and reports merge proof for the delivered branch. Non-integrated exits report `Merge deferred` or `Merge blocked` with explicit handoff commands. |
|
|
34
|
-
| `carson recover --check NAME [--json]` | Run the exceptional governed recovery path when one governance-owned required check is already red on the default branch. Carson proves the named baseline failure, keeps every other gate intact, merges through the recovery path
|
|
33
|
+
| `carson recover --check NAME [--json]` | Run the exceptional governed recovery path when one governance-owned required check is already red on the default branch. Carson proves the named baseline failure, keeps every other gate intact, and merges through the recovery path. |
|
|
35
34
|
| `carson sync` | Fast-forward local `main` from configured remote when tree is clean. |
|
|
36
35
|
| `carson prune` | Remove stale local branches whose upstream refs no longer exist. |
|
|
37
36
|
| `carson housekeep [--json] [--dry-run]` | Attempt to sync the current repo, then reap worktrees with strong abandonment evidence, reconcile integrated delivery worktree records from the ledger, and prune stale branches. Safe cleanup still runs when sync is blocked. |
|
data/MANUAL.md
CHANGED
|
@@ -40,7 +40,7 @@ On first run (no `~/.carson/config.json` exists), `onboard` launches `carson set
|
|
|
40
40
|
- Repository `core.hooksPath` alignment to Carson global hooks.
|
|
41
41
|
- Commit-time governance gate via managed `pre-commit` hook.
|
|
42
42
|
- Canonical `.github/*` template synchronisation (when `lint.canonical` is configured).
|
|
43
|
-
- Initial governance
|
|
43
|
+
- Initial governance status check.
|
|
44
44
|
|
|
45
45
|
### Reconfigure later
|
|
46
46
|
|
|
@@ -78,7 +78,7 @@ jobs:
|
|
|
78
78
|
Notes:
|
|
79
79
|
- When upgrading Carson, update both `carson_ref` and `carson_version` together.
|
|
80
80
|
- `CARSON_READ_TOKEN` must have read access to your policy source repository.
|
|
81
|
-
- The reusable workflow installs a pinned RuboCop gem before
|
|
81
|
+
- The reusable workflow installs a pinned RuboCop gem before lint checks; mirror the same pin in host governance workflows for deterministic checks.
|
|
82
82
|
|
|
83
83
|
### Canonical Templates
|
|
84
84
|
|
|
@@ -112,7 +112,7 @@ Carson discovers files in this directory and syncs them to governed repos. Root
|
|
|
112
112
|
|
|
113
113
|
## Operating Strategies
|
|
114
114
|
|
|
115
|
-
These strategies are the
|
|
115
|
+
These strategies are the design lens for Carson. If behaviour departs from them, either the product model or the implementation needs attention.
|
|
116
116
|
|
|
117
117
|
### Git Strategist
|
|
118
118
|
|
|
@@ -181,7 +181,7 @@ When a governance-owned required check is already red on the default branch, the
|
|
|
181
181
|
carson recover --check "Carson governance"
|
|
182
182
|
```
|
|
183
183
|
|
|
184
|
-
Recovery is narrow. Carson proves that the named check is red on the default branch, verifies that the current branch is repairing the governance surface, requires every other required check and the review gate to pass, then records a
|
|
184
|
+
Recovery is narrow. Carson proves that the named check is red on the default branch, verifies that the current branch is repairing the governance surface, requires every other required check and the review gate to pass, then records a recovery event before reporting success.
|
|
185
185
|
|
|
186
186
|
If Carson refuses recovery, the message explains the exact missing proof or remaining gate and tells you what to do next.
|
|
187
187
|
|
|
@@ -255,13 +255,13 @@ The two tools serve different layers. EnterWorktree owns the session (CWD switch
|
|
|
255
255
|
|
|
256
256
|
```bash
|
|
257
257
|
carson sync # fast-forward local main
|
|
258
|
-
carson
|
|
258
|
+
carson status # check repository state
|
|
259
259
|
```
|
|
260
260
|
|
|
261
261
|
**Before push or PR update:**
|
|
262
262
|
|
|
263
263
|
```bash
|
|
264
|
-
carson
|
|
264
|
+
carson status
|
|
265
265
|
carson template check
|
|
266
266
|
```
|
|
267
267
|
|
|
@@ -287,7 +287,7 @@ carson list --json # machine-readable output
|
|
|
287
287
|
**Portfolio maintenance:**
|
|
288
288
|
|
|
289
289
|
```bash
|
|
290
|
-
carson refresh # re-apply hooks
|
|
290
|
+
carson refresh # re-apply hooks and templates across all repos
|
|
291
291
|
carson list # list all governed repositories
|
|
292
292
|
```
|
|
293
293
|
|
|
@@ -411,7 +411,7 @@ Change: `CARSON_REVIEW_DISPOSITION`.
|
|
|
411
411
|
|
|
412
412
|
How much Carson prints.
|
|
413
413
|
|
|
414
|
-
- Default: **concise**. A healthy
|
|
414
|
+
- Default: **concise**. A healthy run prints one line. Problems print actionable summaries with cause and fix.
|
|
415
415
|
- `--verbose` restores full diagnostic key-value output for debugging.
|
|
416
416
|
|
|
417
417
|
## Configuration
|
data/README.md
CHANGED
|
@@ -66,7 +66,7 @@ carson housekeep
|
|
|
66
66
|
|
|
67
67
|
`carson deliver` runs Carson-owned branch delivery. Before any push, Carson verifies that the branch is fresh against the configured remote `main` using local git. If freshness is behind or unknown, delivery stops with an explicit block and no PR side effect. After a PR exists, Carson delegates merge eligibility to GitHub's `mergeStateStatus` — if GitHub reports `CLEAN`, Carson proceeds regardless of local ancestor status. Plain `deliver` transports existing commits only; `carson deliver --commit "..."` creates one all-dirty delivery commit first, then continues the same flow. If the branch is fresh, Carson pushes it, creates or refreshes the PR, watches the delivery for a bounded settle window, merges when clear, and syncs local `main`. If the settle window expires without integration, Carson exits with an explicit `Merge deferred` or `Merge blocked` handoff instead of leaving the PR mysteriously open. Deferred and blocked exits say whether Carson attempted merge and list the next commands in order.
|
|
68
68
|
|
|
69
|
-
When one Carson-governed required check is already red on the default branch and the current PR is the repair, use `carson recover --check "..."`. Recovery is the explicit exceptional path: Carson proves the baseline failure, keeps every other gate intact, records
|
|
69
|
+
When one Carson-governed required check is already red on the default branch and the current PR is the repair, use `carson recover --check "..."`. Recovery is the explicit exceptional path: Carson proves the baseline failure, keeps every other gate intact, records a recovery event, and never teaches operators to step outside Carson first.
|
|
70
70
|
|
|
71
71
|
`carson worktree list` is the visibility surface for cleanup: it shows every registered worktree, the branch, PR state, whether the content is already on `main`, and Carson's keep or reap recommendation. Content matching `main` is diagnostic, not standalone proof that a worktree is abandoned. `carson housekeep` is the main cleanup pass: sync main, reap worktrees with strong abandonment evidence (for example a missing directory, merged PR, or closed abandoned PR), and prune stale branches. When work needs to be abandoned instead of landed, use `carson abandon <pr-number|pr-url|branch>` to close the PR and clean up the branch/worktree safely.
|
|
72
72
|
|
data/RELEASE.md
CHANGED
|
@@ -7,6 +7,24 @@ Release-note scope rule:
|
|
|
7
7
|
|
|
8
8
|
## Unreleased
|
|
9
9
|
|
|
10
|
+
### Removed
|
|
11
|
+
|
|
12
|
+
- **`carson audit` dissolved.** The audit command and its pre-commit gate have been removed entirely. Three of its seven checks were already enforced by the commands that need them (sealed workbench by `pack!`, working tree by Courier, main sync by `sync!`). The remaining four (PR status, CI baseline, hooks health, outsider fingerprints) were either unproven at commit time or already called by individual commands. Bare `carson` now defaults to `status` instead of `audit`. The pre-commit hook is now a no-op reserved for future use.
|
|
13
|
+
|
|
14
|
+
### Why
|
|
15
|
+
|
|
16
|
+
Audit was a remote-centred pre-commit gate — it blocked commits based on delivery-time concerns (PR checks, CI baseline) that don't belong at commit time. In local-centred mode it was purely dead weight. Rather than relocate it to bureau, we verified that none of its unique checks had proven value at any gate, and dissolved the whole thing. Scars, not speculation.
|
|
17
|
+
|
|
18
|
+
## 4.3.2
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- **Vault gives correct recovery when main worktree is dirty.** `vault_blocked` now inspects stderr from `git merge --ff-only` to distinguish dirty-tree conflicts from diverged-history blocks. Previously, both cases got the generic "rebase" advice — misleading when the branch was already a valid fast-forward descendant and the real problem was uncommitted files in the main worktree.
|
|
23
|
+
|
|
24
|
+
### Why
|
|
25
|
+
|
|
26
|
+
An agent rebased three times on an already-correct branch because Carson misdiagnosed a dirty-tree block as divergence. The fix: read stderr, give the right advice. Scars, not speculation.
|
|
27
|
+
|
|
10
28
|
## 4.3.1
|
|
11
29
|
|
|
12
30
|
### Changed
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
4.3.
|
|
1
|
+
4.3.2
|
data/config/hooks/pre-commit
CHANGED
|
@@ -1,19 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
set -euo pipefail
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
carson_command=( "$CARSON_BIN" )
|
|
6
|
-
elif command -v carson >/dev/null 2>&1; then
|
|
7
|
-
carson_command=( "carson" )
|
|
8
|
-
else
|
|
9
|
-
echo "Carson policy: 'carson' command is required for pre-commit governance checks." >&2
|
|
10
|
-
echo "Install Carson and rerun 'carson prepare'." >&2
|
|
11
|
-
exit 1
|
|
12
|
-
fi
|
|
13
|
-
|
|
14
|
-
if ! "${carson_command[@]}" audit; then
|
|
15
|
-
echo "Carson policy: commit blocked by governance audit." >&2
|
|
16
|
-
echo "Resolve reported policy blocks before committing." >&2
|
|
17
|
-
echo "If lint policy files are missing, run: carson lint policy --source <path-or-git-url>" >&2
|
|
18
|
-
exit 1
|
|
19
|
-
fi
|
|
3
|
+
# Pre-commit hook — reserved for future use.
|
|
4
|
+
exit 0
|
|
@@ -3,7 +3,7 @@ module Carson
|
|
|
3
3
|
module Adapters
|
|
4
4
|
module Agent
|
|
5
5
|
WorkOrder = Struct.new( :repo, :branch, :pr_number, :objective, :context, :acceptance_checks, keyword_init: true )
|
|
6
|
-
# objective: "fix_ci" | "address_review"
|
|
6
|
+
# objective: "fix_ci" | "address_review"
|
|
7
7
|
# context: String (legacy — PR title) or Hash with structured evidence:
|
|
8
8
|
# fix_ci: { title:, ci_logs:, ci_run_url:, prior_attempt: { summary:, dispatched_at: } }
|
|
9
9
|
# address_review: { title:, review_findings: [{ kind:, url:, body: }], prior_attempt: ... }
|
data/lib/carson/config.rb
CHANGED
|
@@ -28,7 +28,6 @@ module Carson
|
|
|
28
28
|
:review_wait_seconds, :review_poll_seconds, :review_max_polls, :review_sweep_window_days,
|
|
29
29
|
:review_sweep_states, :review_disposition, :review_risk_keywords,
|
|
30
30
|
:review_tracking_issue_title, :review_tracking_issue_label, :review_bot_usernames,
|
|
31
|
-
:audit_advisory_check_names,
|
|
32
31
|
:workflow_style,
|
|
33
32
|
:govern_repos, :govern_merge_method,
|
|
34
33
|
:govern_agent_provider, :govern_state_path,
|
|
@@ -80,9 +79,6 @@ module Carson
|
|
|
80
79
|
"label" => "carson-review-sweep"
|
|
81
80
|
}
|
|
82
81
|
},
|
|
83
|
-
"audit" => {
|
|
84
|
-
"advisory_check_names" => [ "Scheduled review sweep", "Carson governance", "Tag, release, publish" ]
|
|
85
|
-
},
|
|
86
82
|
"deliver" => {
|
|
87
83
|
"poll_interval_at_bureau" => 30
|
|
88
84
|
},
|
|
@@ -170,9 +166,6 @@ module Carson
|
|
|
170
166
|
sweep[ "states" ] = states unless states.empty?
|
|
171
167
|
bot_usernames = env_string_array( key: "CARSON_REVIEW_BOT_USERNAMES" )
|
|
172
168
|
review[ "bot_usernames" ] = bot_usernames unless bot_usernames.empty?
|
|
173
|
-
audit = fetch_hash_section( data: copy, key: "audit" )
|
|
174
|
-
advisory_names = env_string_array( key: "CARSON_AUDIT_ADVISORY_CHECK_NAMES" )
|
|
175
|
-
audit[ "advisory_check_names" ] = advisory_names unless advisory_names.empty?
|
|
176
169
|
deliver = fetch_hash_section( data: copy, key: "deliver" )
|
|
177
170
|
deliver[ "poll_interval_at_bureau" ] = env_integer( key: "CARSON_POLL_INTERVAL_AT_BUREAU", fallback: deliver.fetch( "poll_interval_at_bureau" ) )
|
|
178
171
|
govern = fetch_hash_section( data: copy, key: "govern" )
|
|
@@ -243,9 +236,6 @@ module Carson
|
|
|
243
236
|
@review_tracking_issue_title = fetch_string( hash: tracking_issue_hash, key: "title" )
|
|
244
237
|
@review_tracking_issue_label = fetch_string( hash: tracking_issue_hash, key: "label" )
|
|
245
238
|
@review_bot_usernames = fetch_optional_string_array( hash: review_hash, key: "bot_usernames" )
|
|
246
|
-
audit_hash = fetch_hash( hash: data, key: "audit" )
|
|
247
|
-
@audit_advisory_check_names = fetch_optional_string_array( hash: audit_hash, key: "advisory_check_names" )
|
|
248
|
-
|
|
249
239
|
deliver_hash = fetch_hash( hash: data, key: "deliver" )
|
|
250
240
|
@poll_interval_at_bureau = fetch_non_negative_integer( hash: deliver_hash, key: "poll_interval_at_bureau" )
|
|
251
241
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# Repository onboarding and refresh lifecycle.
|
|
2
|
-
# Onboard: detect remote, install hooks, apply templates
|
|
2
|
+
# Onboard: detect remote, install hooks, apply templates.
|
|
3
3
|
# Refresh: re-apply hooks and templates after Carson upgrade.
|
|
4
4
|
# Refresh all: batch refresh across governed portfolio with safety checks.
|
|
5
5
|
module Carson
|
|
6
6
|
class Runtime
|
|
7
7
|
module Local
|
|
8
8
|
# One-command onboarding for new repositories: detect remote, install hooks,
|
|
9
|
-
# apply templates
|
|
9
|
+
# and apply templates.
|
|
10
10
|
def onboard!
|
|
11
11
|
fingerprint_status = block_if_outsider_fingerprints!
|
|
12
12
|
return fingerprint_status unless fingerprint_status.nil?
|
|
@@ -32,7 +32,7 @@ module Carson
|
|
|
32
32
|
onboard_apply!
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
-
# Re-applies hooks
|
|
35
|
+
# Re-applies hooks and templates after upgrading Carson.
|
|
36
36
|
def refresh!
|
|
37
37
|
fingerprint_status = block_if_outsider_fingerprints!
|
|
38
38
|
return fingerprint_status unless fingerprint_status.nil?
|
|
@@ -55,13 +55,8 @@ module Carson
|
|
|
55
55
|
|
|
56
56
|
@template_sync_result = template_propagate!( drift_count: drift_count + stale_count )
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
puts_line "OK: Carson refresh completed for #{repo_root}."
|
|
61
|
-
elsif audit_status == EXIT_BLOCK
|
|
62
|
-
puts_line "Refresh complete — some checks need attention. Run carson audit for details."
|
|
63
|
-
end
|
|
64
|
-
return audit_status
|
|
58
|
+
puts_line "OK: Carson refresh completed for #{repo_root}."
|
|
59
|
+
return EXIT_OK
|
|
65
60
|
end
|
|
66
61
|
|
|
67
62
|
puts_line "Refresh"
|
|
@@ -82,12 +77,11 @@ module Carson
|
|
|
82
77
|
|
|
83
78
|
@template_sync_result = template_propagate!( drift_count: total_drift )
|
|
84
79
|
|
|
85
|
-
audit_status = audit!
|
|
86
80
|
puts_line "Refresh complete."
|
|
87
|
-
|
|
81
|
+
EXIT_OK
|
|
88
82
|
end
|
|
89
83
|
|
|
90
|
-
# Re-applies hooks
|
|
84
|
+
# Re-applies hooks and templates across all governed repositories.
|
|
91
85
|
# Checks each repo for safety (active worktrees, uncommitted changes) and
|
|
92
86
|
# marks unsafe repos as pending to avoid disrupting active work.
|
|
93
87
|
def refresh_all!
|
|
@@ -202,7 +196,7 @@ module Carson
|
|
|
202
196
|
|
|
203
197
|
private
|
|
204
198
|
|
|
205
|
-
# Concise onboard orchestration: hooks, templates, remote,
|
|
199
|
+
# Concise onboard orchestration: hooks, templates, remote, guidance.
|
|
206
200
|
def onboard_apply!
|
|
207
201
|
hook_status = with_captured_output { prepare! }
|
|
208
202
|
return hook_status unless hook_status == EXIT_OK
|
|
@@ -218,7 +212,6 @@ module Carson
|
|
|
218
212
|
end
|
|
219
213
|
|
|
220
214
|
onboard_report_remote!
|
|
221
|
-
audit_status = onboard_run_audit!
|
|
222
215
|
|
|
223
216
|
puts_line ""
|
|
224
217
|
puts_line "Carson at your service."
|
|
@@ -235,7 +228,7 @@ module Carson
|
|
|
235
228
|
puts_line ""
|
|
236
229
|
puts_line "To adjust any setting: carson setup"
|
|
237
230
|
|
|
238
|
-
|
|
231
|
+
EXIT_OK
|
|
239
232
|
end
|
|
240
233
|
|
|
241
234
|
# Friendly remote status for onboard output.
|
|
@@ -247,33 +240,6 @@ module Carson
|
|
|
247
240
|
end
|
|
248
241
|
end
|
|
249
242
|
|
|
250
|
-
# Runs audit with captured output; reports summary instead of full detail.
|
|
251
|
-
def onboard_run_audit!
|
|
252
|
-
audit_error = nil
|
|
253
|
-
audit_status = with_captured_output { audit! }
|
|
254
|
-
rescue StandardError => exception
|
|
255
|
-
audit_error = exception
|
|
256
|
-
audit_status = EXIT_OK
|
|
257
|
-
ensure
|
|
258
|
-
return onboard_print_audit_result( status: audit_status, error: audit_error )
|
|
259
|
-
end
|
|
260
|
-
|
|
261
|
-
def onboard_print_audit_result( status:, error: )
|
|
262
|
-
if error
|
|
263
|
-
if error.message.to_s.match?( /HEAD|rev-parse/ )
|
|
264
|
-
puts_line "No commits yet — run carson audit after your first commit."
|
|
265
|
-
else
|
|
266
|
-
puts_line "Audit skipped — run carson audit for details."
|
|
267
|
-
end
|
|
268
|
-
return EXIT_OK
|
|
269
|
-
end
|
|
270
|
-
|
|
271
|
-
if status == EXIT_BLOCK
|
|
272
|
-
puts_line "Some checks need attention — run carson audit for details."
|
|
273
|
-
end
|
|
274
|
-
status
|
|
275
|
-
end
|
|
276
|
-
|
|
277
243
|
# Verifies configured remote exists and logs status without mutating remotes.
|
|
278
244
|
def report_detected_remote!
|
|
279
245
|
if git_remote_exists?( remote_name: config.git_remote )
|
|
@@ -84,14 +84,14 @@ module Carson
|
|
|
84
84
|
}
|
|
85
85
|
if baseline.fetch( :status ) == "skipped"
|
|
86
86
|
result[ :error ] = "unable to verify the default-branch baseline: #{baseline.fetch( :skip_reason )}"
|
|
87
|
-
result[ :recovery ] = "run carson
|
|
87
|
+
result[ :recovery ] = "run carson status after fixing GitHub access"
|
|
88
88
|
return recover_finish( result: result, exit_code: EXIT_ERROR, json_output: json_output )
|
|
89
89
|
end
|
|
90
90
|
|
|
91
91
|
baseline_entry = recovery_baseline_entry( baseline: baseline, check_name: check_name )
|
|
92
92
|
if baseline_entry.nil?
|
|
93
93
|
result[ :error ] = "#{check_name} is not red on #{baseline.fetch( :default_branch, config.main_branch )}"
|
|
94
|
-
result[ :recovery ] = "run carson
|
|
94
|
+
result[ :recovery ] = "run carson status to confirm the baseline check state"
|
|
95
95
|
return recover_finish( result: result, exit_code: EXIT_BLOCK, json_output: json_output )
|
|
96
96
|
end
|
|
97
97
|
|
|
@@ -408,7 +408,7 @@ module Carson
|
|
|
408
408
|
elsif result[ :synced ]
|
|
409
409
|
puts_line "Synced local #{result.fetch( :main_branch )}."
|
|
410
410
|
end
|
|
411
|
-
puts_line "Recorded recovery
|
|
411
|
+
puts_line "Recorded recovery for #{result.fetch( :check )}."
|
|
412
412
|
puts_line "Check back with #{result.fetch( :next_step )}" if result[ :next_step ]
|
|
413
413
|
end
|
|
414
414
|
end
|
|
@@ -163,7 +163,7 @@ module Carson
|
|
|
163
163
|
}
|
|
164
164
|
end
|
|
165
165
|
|
|
166
|
-
# When sweep is clear, close prior tracking issue and add
|
|
166
|
+
# When sweep is clear, close prior tracking issue and add a clear comment.
|
|
167
167
|
def close_review_sweep_issue_if_open( repo_slug:, issue: )
|
|
168
168
|
return { action: "none", issue: nil } if issue.nil?
|
|
169
169
|
return { action: "none", issue: issue } unless issue.fetch( :state ) == "OPEN"
|
data/lib/carson/runtime.rb
CHANGED
|
@@ -14,8 +14,6 @@ module Carson
|
|
|
14
14
|
EXIT_ERROR = 1
|
|
15
15
|
EXIT_BLOCK = 2
|
|
16
16
|
|
|
17
|
-
REPORT_MD = "pr_report_latest.md".freeze
|
|
18
|
-
REPORT_JSON = "pr_report_latest.json".freeze
|
|
19
17
|
REVIEW_GATE_REPORT_MD = "review_gate_latest.md".freeze
|
|
20
18
|
REVIEW_GATE_REPORT_JSON = "review_gate_latest.json".freeze
|
|
21
19
|
REVIEW_SWEEP_REPORT_MD = "review_sweep_latest.md".freeze
|
|
@@ -43,7 +41,7 @@ module Carson
|
|
|
43
41
|
attr_reader :template_sync_result
|
|
44
42
|
|
|
45
43
|
# Lazy ledger: only constructed when a command actually needs delivery state.
|
|
46
|
-
# Read-only commands (worktree list,
|
|
44
|
+
# Read-only commands (worktree list, prune, sync) never touch the
|
|
47
45
|
# govern state lock file.
|
|
48
46
|
def ledger
|
|
49
47
|
@ledger ||= Ledger.new( path: @config.govern_state_path )
|
|
@@ -115,7 +113,7 @@ module Carson
|
|
|
115
113
|
success
|
|
116
114
|
end
|
|
117
115
|
|
|
118
|
-
# Human-readable plural suffix helper
|
|
116
|
+
# Human-readable plural suffix helper.
|
|
119
117
|
def plural_suffix( count: )
|
|
120
118
|
count.to_i == 1 ? "" : "s"
|
|
121
119
|
end
|
|
@@ -366,7 +364,6 @@ module Carson
|
|
|
366
364
|
end
|
|
367
365
|
|
|
368
366
|
require_relative "runtime/local"
|
|
369
|
-
require_relative "runtime/audit"
|
|
370
367
|
require_relative "runtime/loop_runner"
|
|
371
368
|
require_relative "runtime/housekeep"
|
|
372
369
|
require_relative "runtime/list"
|
|
@@ -63,12 +63,22 @@ module Carson
|
|
|
63
63
|
end
|
|
64
64
|
|
|
65
65
|
# Build the blocked/error result when vault acceptance fails.
|
|
66
|
+
# Distinguishes dirty-tree conflicts from diverged-history blocks
|
|
67
|
+
# so the agent gets the correct recovery advice.
|
|
66
68
|
def vault_blocked( parcel, stderr )
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
69
|
+
if stderr.to_s.include?( "would be overwritten" )
|
|
70
|
+
{
|
|
71
|
+
status: "block",
|
|
72
|
+
error: "Main worktree has uncommitted changes that conflict with #{parcel.label}.",
|
|
73
|
+
recovery: "Commit or discard the dirty files in the main worktree, then deliver again."
|
|
74
|
+
}
|
|
75
|
+
else
|
|
76
|
+
{
|
|
77
|
+
status: "block",
|
|
78
|
+
error: "#{parcel.label} cannot be fast-forwarded into #{@main_label}.",
|
|
79
|
+
recovery: "Rebase onto #{@main_label} and deliver again."
|
|
80
|
+
}
|
|
81
|
+
end
|
|
72
82
|
end
|
|
73
83
|
|
|
74
84
|
end
|
data/lib/cli.rb
CHANGED
|
@@ -5,7 +5,7 @@ require "optparse"
|
|
|
5
5
|
module Carson
|
|
6
6
|
class CLI
|
|
7
7
|
PORTFOLIO_COMMANDS = %w[onboard offboard list refresh version].freeze
|
|
8
|
-
REPO_COMMANDS = %w[deliver receive sync status
|
|
8
|
+
REPO_COMMANDS = %w[deliver receive sync status prune housekeep worktree abandon recover review template setup checkin checkout].freeze
|
|
9
9
|
ALL_COMMANDS = ( PORTFOLIO_COMMANDS + REPO_COMMANDS ).freeze
|
|
10
10
|
|
|
11
11
|
def self.start( arguments:, repo_root:, tool_root:, output:, error: )
|
|
@@ -154,7 +154,7 @@ module Carson
|
|
|
154
154
|
return { command: :help }
|
|
155
155
|
end
|
|
156
156
|
return { command: "version" } if [ "--version", "-v" ].include?( first )
|
|
157
|
-
return { command: "
|
|
157
|
+
return { command: "status" } if arguments.empty?
|
|
158
158
|
|
|
159
159
|
nil
|
|
160
160
|
end
|
|
@@ -217,8 +217,6 @@ module Carson
|
|
|
217
217
|
parse_sync_command( arguments: arguments, error: error )
|
|
218
218
|
when "status"
|
|
219
219
|
parse_status_command( arguments: arguments, error: error )
|
|
220
|
-
when "audit"
|
|
221
|
-
parse_audit_command( arguments: arguments, error: error )
|
|
222
220
|
when "prune"
|
|
223
221
|
parse_prune_command( arguments: arguments, error: error )
|
|
224
222
|
when "housekeep"
|
|
@@ -284,7 +282,7 @@ module Carson
|
|
|
284
282
|
parser.banner = "Usage: carson onboard <REPO_PATH>"
|
|
285
283
|
parser.separator ""
|
|
286
284
|
parser.separator "Register a repository for Carson governance."
|
|
287
|
-
parser.separator "Detects the remote, installs hooks, applies templates
|
|
285
|
+
parser.separator "Detects the remote, installs hooks, and applies templates."
|
|
288
286
|
parser.separator ""
|
|
289
287
|
parser.separator "Examples:"
|
|
290
288
|
parser.separator " carson onboard ~/Dev/app Onboard a specific repository"
|
|
@@ -677,36 +675,6 @@ module Carson
|
|
|
677
675
|
{ command: :invalid }
|
|
678
676
|
end
|
|
679
677
|
|
|
680
|
-
# --- audit ---
|
|
681
|
-
|
|
682
|
-
def self.parse_audit_command( arguments:, error: )
|
|
683
|
-
options = { json: false }
|
|
684
|
-
audit_parser = OptionParser.new do |parser|
|
|
685
|
-
parser.banner = "Usage: carson audit [--json]"
|
|
686
|
-
parser.separator ""
|
|
687
|
-
parser.separator "Run pre-commit health checks on the repository."
|
|
688
|
-
parser.separator "Validates hooks, main-branch sync, PR status, and CI baseline."
|
|
689
|
-
parser.separator "Exits with a non-zero status when policy violations are found."
|
|
690
|
-
parser.separator ""
|
|
691
|
-
parser.separator "Options:"
|
|
692
|
-
parser.on( "--json", "Machine-readable JSON output" ) { options[ :json ] = true }
|
|
693
|
-
parser.separator ""
|
|
694
|
-
parser.separator "Examples:"
|
|
695
|
-
parser.separator " carson audit Check repository health (also the default command)"
|
|
696
|
-
parser.separator " carson audit --json Structured output for agent consumption"
|
|
697
|
-
end
|
|
698
|
-
audit_parser.parse!( arguments )
|
|
699
|
-
unless arguments.empty?
|
|
700
|
-
error.puts "#{BADGE} Unexpected arguments for audit: #{arguments.join( ' ' )}"
|
|
701
|
-
return { command: :invalid }
|
|
702
|
-
end
|
|
703
|
-
{ command: "audit", json: options[ :json ] }
|
|
704
|
-
rescue OptionParser::ParseError => exception
|
|
705
|
-
error.puts "#{BADGE} #{exception.message}"
|
|
706
|
-
error.puts audit_parser
|
|
707
|
-
{ command: :invalid }
|
|
708
|
-
end
|
|
709
|
-
|
|
710
678
|
# --- abandon ---
|
|
711
679
|
|
|
712
680
|
def self.parse_abandon_command( arguments:, error: )
|
|
@@ -854,7 +822,7 @@ module Carson
|
|
|
854
822
|
parser.banner = "Usage: carson recover --check NAME [--json]"
|
|
855
823
|
parser.separator ""
|
|
856
824
|
parser.separator "Merge the current repair PR when one governance-owned required check is already red on the default branch."
|
|
857
|
-
parser.separator "Recovery is narrow: Carson verifies the baseline failure, keeps every other gate intact, and records
|
|
825
|
+
parser.separator "Recovery is narrow: Carson verifies the baseline failure, keeps every other gate intact, and records a recovery event."
|
|
858
826
|
parser.separator ""
|
|
859
827
|
parser.separator "Options:"
|
|
860
828
|
parser.on( "--check NAME", "Name of the governance-owned required check to recover" ) { |value| options[ :check_name ] = value }
|
|
@@ -988,8 +956,6 @@ module Carson
|
|
|
988
956
|
runtime.status!( json_output: parsed.fetch( :json, false ) )
|
|
989
957
|
when "setup"
|
|
990
958
|
runtime.setup!( cli_choices: parsed.fetch( :cli_choices, {} ) )
|
|
991
|
-
when "audit"
|
|
992
|
-
runtime.audit!( json_output: parsed.fetch( :json, false ) )
|
|
993
959
|
when "abandon"
|
|
994
960
|
runtime.abandon!( target: parsed.fetch( :target ), json_output: parsed.fetch( :json, false ) )
|
|
995
961
|
when "sync"
|
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: 4.3.
|
|
4
|
+
version: 4.3.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Hailei Wang
|
|
@@ -57,7 +57,6 @@ files:
|
|
|
57
57
|
- lib/carson/revision.rb
|
|
58
58
|
- lib/carson/runtime.rb
|
|
59
59
|
- lib/carson/runtime/abandon.rb
|
|
60
|
-
- lib/carson/runtime/audit.rb
|
|
61
60
|
- lib/carson/runtime/deliver.rb
|
|
62
61
|
- lib/carson/runtime/housekeep.rb
|
|
63
62
|
- lib/carson/runtime/list.rb
|
data/lib/carson/runtime/audit.rb
DELETED
|
@@ -1,603 +0,0 @@
|
|
|
1
|
-
# Pre-commit audit — checks hooks, main sync, PR checks, and CI baseline.
|
|
2
|
-
# Exits with EXIT_BLOCK when policy violations are found.
|
|
3
|
-
# Supports --json for machine-readable structured output.
|
|
4
|
-
require "cgi"
|
|
5
|
-
|
|
6
|
-
module Carson
|
|
7
|
-
class Runtime
|
|
8
|
-
module Audit
|
|
9
|
-
def audit!( json_output: false )
|
|
10
|
-
# Sealed workbench guard — hard block before anything else.
|
|
11
|
-
# The warehouse seals the workbench when a parcel ships.
|
|
12
|
-
# No commits allowed until the delivery outcome is confirmed.
|
|
13
|
-
sealed_result = audit_sealed_workbench( json_output: json_output )
|
|
14
|
-
return sealed_result unless sealed_result.nil?
|
|
15
|
-
|
|
16
|
-
fingerprint_status = block_if_outsider_fingerprints!
|
|
17
|
-
return fingerprint_status unless fingerprint_status.nil?
|
|
18
|
-
unless head_exists?
|
|
19
|
-
if json_output
|
|
20
|
-
output.puts JSON.pretty_generate( { command: "audit", status: "skipped", reason: "no commits yet", exit_code: EXIT_OK } )
|
|
21
|
-
else
|
|
22
|
-
puts_line "No commits yet — audit skipped for initial commit."
|
|
23
|
-
end
|
|
24
|
-
return EXIT_OK
|
|
25
|
-
end
|
|
26
|
-
audit_state = "ok"
|
|
27
|
-
audit_concise_problems = []
|
|
28
|
-
puts_verbose ""
|
|
29
|
-
puts_verbose "[Repository]"
|
|
30
|
-
puts_verbose "root: #{repo_root}"
|
|
31
|
-
puts_verbose "current_branch: #{current_branch}"
|
|
32
|
-
puts_verbose ""
|
|
33
|
-
puts_verbose "[Working Tree]"
|
|
34
|
-
puts_verbose git_capture!( "status", "--short", "--branch" ).strip
|
|
35
|
-
working_tree = audit_working_tree_report
|
|
36
|
-
if working_tree.fetch( :status ) == "block"
|
|
37
|
-
puts_verbose "ACTION: #{working_tree.fetch( :error )}; #{working_tree.fetch( :recovery )}."
|
|
38
|
-
audit_state = "block"
|
|
39
|
-
audit_concise_problems << "Working tree: #{working_tree.fetch( :error )} — #{working_tree.fetch( :recovery )}."
|
|
40
|
-
end
|
|
41
|
-
puts_verbose ""
|
|
42
|
-
puts_verbose "[Hooks]"
|
|
43
|
-
hooks_ok = hooks_health_report
|
|
44
|
-
hooks_status = hooks_ok ? "ok" : "mismatch"
|
|
45
|
-
unless hooks_ok
|
|
46
|
-
audit_state = "block"
|
|
47
|
-
audit_concise_problems << "Hooks don't match — run carson refresh."
|
|
48
|
-
end
|
|
49
|
-
puts_verbose ""
|
|
50
|
-
puts_verbose "[Main Sync Status]"
|
|
51
|
-
ahead_count, behind_count, main_error = main_sync_counts
|
|
52
|
-
main_sync = { ahead: 0, behind: 0, status: "ok" }
|
|
53
|
-
if main_error
|
|
54
|
-
puts_verbose "main_vs_remote_main: unknown"
|
|
55
|
-
puts_verbose "WARN: unable to calculate main sync status (#{main_error})."
|
|
56
|
-
audit_state = "attention" if audit_state == "ok"
|
|
57
|
-
audit_concise_problems << "Main sync: unable to determine — check remote connectivity."
|
|
58
|
-
main_sync = { ahead: 0, behind: 0, status: "unknown", error: main_error }
|
|
59
|
-
elsif ahead_count.positive?
|
|
60
|
-
puts_verbose "main_vs_remote_main_ahead: #{ahead_count}"
|
|
61
|
-
puts_verbose "main_vs_remote_main_behind: #{behind_count}"
|
|
62
|
-
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."
|
|
63
|
-
audit_state = "block"
|
|
64
|
-
audit_concise_problems << "Main sync (#{config.git_remote}): ahead by #{ahead_count} — git fetch #{config.git_remote}, or carson setup to switch remote."
|
|
65
|
-
main_sync = { ahead: ahead_count, behind: behind_count, status: "ahead" }
|
|
66
|
-
elsif behind_count.positive?
|
|
67
|
-
puts_verbose "main_vs_remote_main_ahead: #{ahead_count}"
|
|
68
|
-
puts_verbose "main_vs_remote_main_behind: #{behind_count}"
|
|
69
|
-
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."
|
|
70
|
-
audit_state = "attention" if audit_state == "ok"
|
|
71
|
-
audit_concise_problems << "Main sync (#{config.git_remote}): behind by #{behind_count} — run carson sync."
|
|
72
|
-
main_sync = { ahead: ahead_count, behind: behind_count, status: "behind" }
|
|
73
|
-
else
|
|
74
|
-
puts_verbose "main_vs_remote_main_ahead: 0"
|
|
75
|
-
puts_verbose "main_vs_remote_main_behind: 0"
|
|
76
|
-
puts_verbose "ACTION: local #{config.main_branch} is in sync with #{config.git_remote}/#{config.main_branch}."
|
|
77
|
-
end
|
|
78
|
-
puts_verbose ""
|
|
79
|
-
puts_verbose "[PR and Required Checks (gh)]"
|
|
80
|
-
monitor_report = pr_and_check_report
|
|
81
|
-
audit_state = "attention" if audit_state == "ok" && !%w[ok skipped].include?( monitor_report.fetch( :status ) )
|
|
82
|
-
if monitor_report.fetch( :status ) == "attention"
|
|
83
|
-
checks = monitor_report.fetch( :checks )
|
|
84
|
-
failing_count = checks.fetch( :failing_count )
|
|
85
|
-
pending_count = checks.fetch( :pending_count )
|
|
86
|
-
total = checks.fetch( :required_total )
|
|
87
|
-
fail_names = checks.fetch( :failing ).map { |entry| entry.fetch( :name ) }.join( ", " )
|
|
88
|
-
if failing_count.positive? && pending_count.positive?
|
|
89
|
-
audit_concise_problems << "Checks: #{failing_count} failing (#{fail_names}), #{pending_count} pending of #{total} required."
|
|
90
|
-
elsif failing_count.positive?
|
|
91
|
-
audit_concise_problems << "Checks: #{failing_count} of #{total} failing (#{fail_names})."
|
|
92
|
-
elsif pending_count.positive?
|
|
93
|
-
audit_concise_problems << "Checks: pending (#{total - pending_count} of #{total} complete)."
|
|
94
|
-
end
|
|
95
|
-
end
|
|
96
|
-
puts_verbose ""
|
|
97
|
-
puts_verbose "[Default Branch CI Baseline (gh)]"
|
|
98
|
-
default_branch_baseline = default_branch_ci_baseline_report
|
|
99
|
-
audit_state = "attention" if audit_state == "ok" && !%w[ok skipped].include?( default_branch_baseline.fetch( :status ) )
|
|
100
|
-
baseline_status = default_branch_baseline.fetch( :status )
|
|
101
|
-
if baseline_status == "block"
|
|
102
|
-
parts = []
|
|
103
|
-
if default_branch_baseline.fetch( :failing_count ).positive?
|
|
104
|
-
names = default_branch_baseline.fetch( :failing ).map { |entry| entry.fetch( :name ) }.join( ", " )
|
|
105
|
-
parts << "#{default_branch_baseline.fetch( :failing_count )} failing (#{names})"
|
|
106
|
-
end
|
|
107
|
-
if default_branch_baseline.fetch( :pending_count ).positive?
|
|
108
|
-
names = default_branch_baseline.fetch( :pending ).map { |entry| entry.fetch( :name ) }.join( ", " )
|
|
109
|
-
parts << "#{default_branch_baseline.fetch( :pending_count )} pending (#{names})"
|
|
110
|
-
end
|
|
111
|
-
parts << "no check-runs for active workflows" if default_branch_baseline.fetch( :no_check_evidence )
|
|
112
|
-
audit_concise_problems << "Baseline (#{default_branch_baseline.fetch( :default_branch, config.main_branch )}): #{parts.join( ', ' )} — fix before merge."
|
|
113
|
-
elsif baseline_status == "attention"
|
|
114
|
-
parts = []
|
|
115
|
-
if default_branch_baseline.fetch( :advisory_failing_count ).positive?
|
|
116
|
-
names = default_branch_baseline.fetch( :advisory_failing ).map { |entry| entry.fetch( :name ) }.join( ", " )
|
|
117
|
-
parts << "#{default_branch_baseline.fetch( :advisory_failing_count )} advisory failing (#{names})"
|
|
118
|
-
end
|
|
119
|
-
if default_branch_baseline.fetch( :advisory_pending_count ).positive?
|
|
120
|
-
names = default_branch_baseline.fetch( :advisory_pending ).map { |entry| entry.fetch( :name ) }.join( ", " )
|
|
121
|
-
parts << "#{default_branch_baseline.fetch( :advisory_pending_count )} advisory pending (#{names})"
|
|
122
|
-
end
|
|
123
|
-
audit_concise_problems << "Baseline (#{default_branch_baseline.fetch( :default_branch, config.main_branch )}): #{parts.join( ', ' )}."
|
|
124
|
-
end
|
|
125
|
-
if config.lint_canonical.nil? || config.lint_canonical.to_s.empty?
|
|
126
|
-
puts_verbose ""
|
|
127
|
-
puts_verbose "[Canonical Lint Policy]"
|
|
128
|
-
puts_verbose "HINT: lint.canonical not configured — run carson setup to enable."
|
|
129
|
-
end
|
|
130
|
-
write_and_print_pr_monitor_report(
|
|
131
|
-
report: monitor_report.merge(
|
|
132
|
-
default_branch_baseline: default_branch_baseline,
|
|
133
|
-
audit_status: audit_state
|
|
134
|
-
)
|
|
135
|
-
)
|
|
136
|
-
exit_code = audit_state == "block" ? EXIT_BLOCK : EXIT_OK
|
|
137
|
-
|
|
138
|
-
if json_output
|
|
139
|
-
result = {
|
|
140
|
-
command: "audit",
|
|
141
|
-
status: audit_state,
|
|
142
|
-
branch: current_branch,
|
|
143
|
-
working_tree: working_tree,
|
|
144
|
-
hooks: { status: hooks_status },
|
|
145
|
-
main_sync: main_sync,
|
|
146
|
-
pr: monitor_report[ :pr ],
|
|
147
|
-
checks: monitor_report.fetch( :checks ),
|
|
148
|
-
baseline: {
|
|
149
|
-
status: default_branch_baseline.fetch( :status ),
|
|
150
|
-
repository: default_branch_baseline[ :repository ],
|
|
151
|
-
failing_count: default_branch_baseline.fetch( :failing_count ),
|
|
152
|
-
pending_count: default_branch_baseline.fetch( :pending_count ),
|
|
153
|
-
advisory_failing_count: default_branch_baseline.fetch( :advisory_failing_count ),
|
|
154
|
-
advisory_pending_count: default_branch_baseline.fetch( :advisory_pending_count )
|
|
155
|
-
},
|
|
156
|
-
problems: audit_concise_problems,
|
|
157
|
-
exit_code: exit_code
|
|
158
|
-
}
|
|
159
|
-
output.puts JSON.pretty_generate( result )
|
|
160
|
-
else
|
|
161
|
-
puts_verbose ""
|
|
162
|
-
puts_verbose "[Audit Result]"
|
|
163
|
-
puts_verbose "status: #{audit_state}"
|
|
164
|
-
puts_verbose( audit_state == "block" ? "ACTION: local policy block must be resolved before commit/push." : "ACTION: no local hard block detected." )
|
|
165
|
-
unless verbose?
|
|
166
|
-
audit_concise_problems.each { |problem| puts_line problem }
|
|
167
|
-
puts_line format_audit_state( audit_state )
|
|
168
|
-
end
|
|
169
|
-
end
|
|
170
|
-
exit_code
|
|
171
|
-
end
|
|
172
|
-
|
|
173
|
-
# rubocop:disable Layout/AccessModifierIndentation -- tab-width calculation produces unfixable mixed tabs+spaces
|
|
174
|
-
private
|
|
175
|
-
# rubocop:enable Layout/AccessModifierIndentation
|
|
176
|
-
|
|
177
|
-
def format_audit_state( state )
|
|
178
|
-
case state
|
|
179
|
-
when "ok" then "Audit passed."
|
|
180
|
-
when "block" then "Audit blocked."
|
|
181
|
-
when "attention" then "Audit: needs attention."
|
|
182
|
-
else "Audit: #{state}"
|
|
183
|
-
end
|
|
184
|
-
end
|
|
185
|
-
|
|
186
|
-
# Check if the workbench is sealed (parcel in flight).
|
|
187
|
-
# Returns EXIT_BLOCK if sealed, nil otherwise.
|
|
188
|
-
# Delegates to Warehouse so the seal path is resolved in one place.
|
|
189
|
-
def audit_sealed_workbench( json_output: )
|
|
190
|
-
warehouse = Warehouse.new( path: work_dir )
|
|
191
|
-
return nil unless warehouse.sealed?
|
|
192
|
-
|
|
193
|
-
tracking_number = warehouse.sealed_tracking_number || "unknown"
|
|
194
|
-
if json_output
|
|
195
|
-
require "json"
|
|
196
|
-
output.puts JSON.pretty_generate( {
|
|
197
|
-
command: "audit",
|
|
198
|
-
status: "block",
|
|
199
|
-
reason: "workbench_sealed",
|
|
200
|
-
tracking_number: tracking_number,
|
|
201
|
-
recovery: "carson worktree create <name>",
|
|
202
|
-
exit_code: EXIT_BLOCK
|
|
203
|
-
} )
|
|
204
|
-
else
|
|
205
|
-
puts_line "Branch is locked — PR ##{tracking_number} in flight."
|
|
206
|
-
puts_line " \u2192 carson worktree create <name>"
|
|
207
|
-
end
|
|
208
|
-
EXIT_BLOCK
|
|
209
|
-
end
|
|
210
|
-
|
|
211
|
-
def audit_working_tree_report
|
|
212
|
-
dirty_reason = dirty_worktree_reason
|
|
213
|
-
return { dirty: false, context: nil, status: "ok" } if dirty_reason.nil?
|
|
214
|
-
|
|
215
|
-
if dirty_reason == "main_worktree"
|
|
216
|
-
{
|
|
217
|
-
dirty: true,
|
|
218
|
-
context: dirty_reason,
|
|
219
|
-
status: "block",
|
|
220
|
-
error: "main working tree has uncommitted changes",
|
|
221
|
-
recovery: "create a worktree with carson worktree create <name>"
|
|
222
|
-
}
|
|
223
|
-
else
|
|
224
|
-
{ dirty: true, context: dirty_reason, status: "ok" }
|
|
225
|
-
end
|
|
226
|
-
end
|
|
227
|
-
|
|
228
|
-
def pr_and_check_report
|
|
229
|
-
report = {
|
|
230
|
-
generated_at: Time.now.utc.iso8601,
|
|
231
|
-
branch: current_branch,
|
|
232
|
-
status: "ok",
|
|
233
|
-
skip_reason: nil,
|
|
234
|
-
pr: nil,
|
|
235
|
-
checks: {
|
|
236
|
-
status: "unknown",
|
|
237
|
-
skip_reason: nil,
|
|
238
|
-
required_total: 0,
|
|
239
|
-
failing_count: 0,
|
|
240
|
-
pending_count: 0,
|
|
241
|
-
failing: [],
|
|
242
|
-
pending: []
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
unless gh_available?
|
|
246
|
-
report[ :status ] = "skipped"
|
|
247
|
-
report[ :skip_reason ] = "gh CLI not available in PATH"
|
|
248
|
-
puts_verbose "SKIP: #{report.fetch( :skip_reason )}"
|
|
249
|
-
return report
|
|
250
|
-
end
|
|
251
|
-
pr_stdout, pr_stderr, pr_success, = gh_run( "pr", "view", current_branch, "--json", "number,title,url,state,reviewDecision" )
|
|
252
|
-
unless pr_success
|
|
253
|
-
error_text = gh_error_text( stdout_text: pr_stdout, stderr_text: pr_stderr, fallback: "unable to read PR for branch #{current_branch}" )
|
|
254
|
-
report[ :status ] = "skipped"
|
|
255
|
-
report[ :skip_reason ] = error_text
|
|
256
|
-
puts_verbose "SKIP: #{error_text}"
|
|
257
|
-
return report
|
|
258
|
-
end
|
|
259
|
-
pr_data = JSON.parse( pr_stdout )
|
|
260
|
-
report[ :pr ] = {
|
|
261
|
-
number: pr_data[ "number" ],
|
|
262
|
-
title: pr_data[ "title" ].to_s,
|
|
263
|
-
url: pr_data[ "url" ].to_s,
|
|
264
|
-
state: pr_data[ "state" ].to_s,
|
|
265
|
-
review_decision: blank_to( value: pr_data[ "reviewDecision" ], default: "NONE" )
|
|
266
|
-
}
|
|
267
|
-
puts_verbose "pr: ##{report.dig( :pr, :number )} #{report.dig( :pr, :title )}"
|
|
268
|
-
puts_verbose "url: #{report.dig( :pr, :url )}"
|
|
269
|
-
puts_verbose "review_decision: #{report.dig( :pr, :review_decision )}"
|
|
270
|
-
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" )
|
|
271
|
-
if checks_stdout.to_s.strip.empty?
|
|
272
|
-
error_text = gh_error_text( stdout_text: checks_stdout, stderr_text: checks_stderr, fallback: "required checks unavailable" )
|
|
273
|
-
report[ :checks ][ :status ] = "skipped"
|
|
274
|
-
report[ :checks ][ :skip_reason ] = error_text
|
|
275
|
-
report[ :status ] = "attention"
|
|
276
|
-
puts_verbose "checks: SKIP (#{error_text})"
|
|
277
|
-
return report
|
|
278
|
-
end
|
|
279
|
-
checks_data = JSON.parse( checks_stdout )
|
|
280
|
-
pending = checks_data.select { |entry| entry[ "bucket" ].to_s == "pending" }
|
|
281
|
-
failing = checks_data.select { |entry| check_entry_failing?( entry: entry ) }
|
|
282
|
-
report[ :checks ][ :status ] = checks_success ? "ok" : ( checks_exit == 8 ? "pending" : "attention" )
|
|
283
|
-
report[ :checks ][ :required_total ] = checks_data.count
|
|
284
|
-
report[ :checks ][ :failing_count ] = failing.count
|
|
285
|
-
report[ :checks ][ :pending_count ] = pending.count
|
|
286
|
-
report[ :checks ][ :failing ] = normalise_check_entries( entries: failing )
|
|
287
|
-
report[ :checks ][ :pending ] = normalise_check_entries( entries: pending )
|
|
288
|
-
puts_verbose "required_checks_total: #{report.dig( :checks, :required_total )}"
|
|
289
|
-
puts_verbose "required_checks_failing: #{report.dig( :checks, :failing_count )}"
|
|
290
|
-
puts_verbose "required_checks_pending: #{report.dig( :checks, :pending_count )}"
|
|
291
|
-
report.dig( :checks, :failing ).each { |entry| puts_verbose "check_fail: #{entry.fetch( :workflow )} / #{entry.fetch( :name )} #{entry.fetch( :link )}".strip }
|
|
292
|
-
report.dig( :checks, :pending ).each { |entry| puts_verbose "check_pending: #{entry.fetch( :workflow )} / #{entry.fetch( :name )} #{entry.fetch( :link )}".strip }
|
|
293
|
-
report[ :status ] = "attention" if report.dig( :checks, :failing_count ).positive? || report.dig( :checks, :pending_count ).positive?
|
|
294
|
-
report
|
|
295
|
-
rescue JSON::ParserError => exception
|
|
296
|
-
report[ :status ] = "skipped"
|
|
297
|
-
report[ :skip_reason ] = "invalid gh JSON response (#{exception.message})"
|
|
298
|
-
puts_verbose "SKIP: #{report.fetch( :skip_reason )}"
|
|
299
|
-
report
|
|
300
|
-
end
|
|
301
|
-
|
|
302
|
-
# Evaluates default-branch CI health so stale workflow drift blocks before merge.
|
|
303
|
-
def default_branch_ci_baseline_report
|
|
304
|
-
report = {
|
|
305
|
-
status: "ok",
|
|
306
|
-
skip_reason: nil,
|
|
307
|
-
repository: nil,
|
|
308
|
-
default_branch: nil,
|
|
309
|
-
head_sha: nil,
|
|
310
|
-
workflows_total: 0,
|
|
311
|
-
check_runs_total: 0,
|
|
312
|
-
failing_count: 0,
|
|
313
|
-
pending_count: 0,
|
|
314
|
-
advisory_failing_count: 0,
|
|
315
|
-
advisory_pending_count: 0,
|
|
316
|
-
no_check_evidence: false,
|
|
317
|
-
failing: [],
|
|
318
|
-
pending: [],
|
|
319
|
-
advisory_failing: [],
|
|
320
|
-
advisory_pending: []
|
|
321
|
-
}
|
|
322
|
-
unless gh_available?
|
|
323
|
-
report[ :status ] = "skipped"
|
|
324
|
-
report[ :skip_reason ] = "gh CLI not available in PATH"
|
|
325
|
-
puts_verbose "baseline: SKIP (#{report.fetch( :skip_reason )})"
|
|
326
|
-
return report
|
|
327
|
-
end
|
|
328
|
-
owner, repo = repository_coordinates
|
|
329
|
-
report[ :repository ] = "#{owner}/#{repo}"
|
|
330
|
-
repository_data = gh_json_payload!(
|
|
331
|
-
"api", "repos/#{owner}/#{repo}",
|
|
332
|
-
"--method", "GET",
|
|
333
|
-
fallback: "unable to read repository metadata for #{owner}/#{repo}"
|
|
334
|
-
)
|
|
335
|
-
default_branch = blank_to( value: repository_data[ "default_branch" ], default: config.main_branch )
|
|
336
|
-
report[ :default_branch ] = default_branch
|
|
337
|
-
branch_data = gh_json_payload!(
|
|
338
|
-
"api", "repos/#{owner}/#{repo}/branches/#{CGI.escape( default_branch )}",
|
|
339
|
-
"--method", "GET",
|
|
340
|
-
fallback: "unable to read default branch #{default_branch}"
|
|
341
|
-
)
|
|
342
|
-
head_sha = branch_data.dig( "commit", "sha" ).to_s.strip
|
|
343
|
-
raise "default branch #{default_branch} has no commit SHA" if head_sha.empty?
|
|
344
|
-
report[ :head_sha ] = head_sha
|
|
345
|
-
workflow_entries = default_branch_workflow_entries(
|
|
346
|
-
owner: owner,
|
|
347
|
-
repo: repo,
|
|
348
|
-
default_branch: default_branch
|
|
349
|
-
)
|
|
350
|
-
report[ :workflows_total ] = workflow_entries.count
|
|
351
|
-
check_runs_payload = gh_json_payload!(
|
|
352
|
-
"api", "repos/#{owner}/#{repo}/commits/#{head_sha}/check-runs",
|
|
353
|
-
"--method", "GET",
|
|
354
|
-
fallback: "unable to read check-runs for #{default_branch}@#{head_sha}"
|
|
355
|
-
)
|
|
356
|
-
check_runs = Array( check_runs_payload[ "check_runs" ] )
|
|
357
|
-
failing, pending = partition_default_branch_check_runs( check_runs: check_runs )
|
|
358
|
-
advisory_names = config.audit_advisory_check_names
|
|
359
|
-
critical_failing, advisory_failing = separate_advisory_check_entries( entries: failing, advisory_names: advisory_names )
|
|
360
|
-
critical_pending, advisory_pending = separate_advisory_check_entries( entries: pending, advisory_names: advisory_names )
|
|
361
|
-
report[ :check_runs_total ] = check_runs.count
|
|
362
|
-
report[ :failing ] = normalise_default_branch_check_entries( entries: critical_failing )
|
|
363
|
-
report[ :pending ] = normalise_default_branch_check_entries( entries: critical_pending )
|
|
364
|
-
report[ :advisory_failing ] = normalise_default_branch_check_entries( entries: advisory_failing )
|
|
365
|
-
report[ :advisory_pending ] = normalise_default_branch_check_entries( entries: advisory_pending )
|
|
366
|
-
report[ :failing_count ] = report.fetch( :failing ).count
|
|
367
|
-
report[ :pending_count ] = report.fetch( :pending ).count
|
|
368
|
-
report[ :advisory_failing_count ] = report.fetch( :advisory_failing ).count
|
|
369
|
-
report[ :advisory_pending_count ] = report.fetch( :advisory_pending ).count
|
|
370
|
-
report[ :no_check_evidence ] = report.fetch( :workflows_total ).positive? && report.fetch( :check_runs_total ).zero?
|
|
371
|
-
report[ :status ] = "block" if report.fetch( :failing_count ).positive?
|
|
372
|
-
report[ :status ] = "block" if report.fetch( :pending_count ).positive?
|
|
373
|
-
report[ :status ] = "block" if report.fetch( :no_check_evidence )
|
|
374
|
-
report[ :status ] = "attention" if report.fetch( :status ) == "ok" && ( report.fetch( :advisory_failing_count ).positive? || report.fetch( :advisory_pending_count ).positive? )
|
|
375
|
-
puts_verbose "default_branch_repository: #{report.fetch( :repository )}"
|
|
376
|
-
puts_verbose "default_branch_name: #{report.fetch( :default_branch )}"
|
|
377
|
-
puts_verbose "default_branch_head_sha: #{report.fetch( :head_sha )}"
|
|
378
|
-
puts_verbose "default_branch_workflows_total: #{report.fetch( :workflows_total )}"
|
|
379
|
-
puts_verbose "default_branch_check_runs_total: #{report.fetch( :check_runs_total )}"
|
|
380
|
-
puts_verbose "default_branch_failing: #{report.fetch( :failing_count )}"
|
|
381
|
-
puts_verbose "default_branch_pending: #{report.fetch( :pending_count )}"
|
|
382
|
-
puts_verbose "default_branch_advisory_failing: #{report.fetch( :advisory_failing_count )}"
|
|
383
|
-
puts_verbose "default_branch_advisory_pending: #{report.fetch( :advisory_pending_count )}"
|
|
384
|
-
report.fetch( :failing ).each { |entry| puts_verbose "default_branch_check_fail: #{entry.fetch( :workflow )} / #{entry.fetch( :name )} #{entry.fetch( :link )}".strip }
|
|
385
|
-
report.fetch( :pending ).each { |entry| puts_verbose "default_branch_check_pending: #{entry.fetch( :workflow )} / #{entry.fetch( :name )} #{entry.fetch( :link )}".strip }
|
|
386
|
-
report.fetch( :advisory_failing ).each { |entry| puts_verbose "default_branch_check_advisory_fail: #{entry.fetch( :workflow )} / #{entry.fetch( :name )} (advisory) #{entry.fetch( :link )}".strip }
|
|
387
|
-
report.fetch( :advisory_pending ).each { |entry| puts_verbose "default_branch_check_advisory_pending: #{entry.fetch( :workflow )} / #{entry.fetch( :name )} (advisory) #{entry.fetch( :link )}".strip }
|
|
388
|
-
if report.fetch( :no_check_evidence )
|
|
389
|
-
puts_verbose "ACTION: default branch has workflow files but no check-runs; align workflow triggers and branch protection check names."
|
|
390
|
-
end
|
|
391
|
-
report
|
|
392
|
-
rescue JSON::ParserError => exception
|
|
393
|
-
report[ :status ] = "skipped"
|
|
394
|
-
report[ :skip_reason ] = "invalid gh JSON response (#{exception.message})"
|
|
395
|
-
puts_verbose "baseline: SKIP (#{report.fetch( :skip_reason )})"
|
|
396
|
-
report
|
|
397
|
-
rescue StandardError => exception
|
|
398
|
-
report[ :status ] = "skipped"
|
|
399
|
-
report[ :skip_reason ] = exception.message
|
|
400
|
-
puts_verbose "baseline: SKIP (#{report.fetch( :skip_reason )})"
|
|
401
|
-
report
|
|
402
|
-
end
|
|
403
|
-
|
|
404
|
-
# Reads JSON API payloads and raises a detailed error when gh reports non-success.
|
|
405
|
-
def gh_json_payload!( *args, fallback: )
|
|
406
|
-
stdout_text, stderr_text, success, = gh_run( *args )
|
|
407
|
-
unless success
|
|
408
|
-
error_text = gh_error_text( stdout_text: stdout_text, stderr_text: stderr_text, fallback: fallback )
|
|
409
|
-
raise error_text
|
|
410
|
-
end
|
|
411
|
-
JSON.parse( stdout_text )
|
|
412
|
-
end
|
|
413
|
-
|
|
414
|
-
# Reads workflow files from default branch; missing workflow directory is valid and returns none.
|
|
415
|
-
def default_branch_workflow_entries( owner:, repo:, default_branch: )
|
|
416
|
-
stdout_text, stderr_text, success, = gh_run(
|
|
417
|
-
"api", "repos/#{owner}/#{repo}/contents/.github/workflows",
|
|
418
|
-
"--method", "GET",
|
|
419
|
-
"-f", "ref=#{default_branch}"
|
|
420
|
-
)
|
|
421
|
-
unless success
|
|
422
|
-
error_text = gh_error_text(
|
|
423
|
-
stdout_text: stdout_text,
|
|
424
|
-
stderr_text: stderr_text,
|
|
425
|
-
fallback: "unable to read workflow files for #{default_branch}"
|
|
426
|
-
)
|
|
427
|
-
return [] if error_text.match?( /\b404\b/ )
|
|
428
|
-
raise error_text
|
|
429
|
-
end
|
|
430
|
-
payload = JSON.parse( stdout_text )
|
|
431
|
-
Array( payload ).select do |entry|
|
|
432
|
-
entry.is_a?( Hash ) &&
|
|
433
|
-
entry[ "type" ].to_s == "file" &&
|
|
434
|
-
entry[ "name" ].to_s.match?( /\.ya?ml\z/i )
|
|
435
|
-
end
|
|
436
|
-
end
|
|
437
|
-
|
|
438
|
-
# Splits default-branch check-runs into failing and pending policy buckets.
|
|
439
|
-
def partition_default_branch_check_runs( check_runs: )
|
|
440
|
-
failing = []
|
|
441
|
-
pending = []
|
|
442
|
-
Array( check_runs ).each do |entry|
|
|
443
|
-
if default_branch_check_run_failing?( entry: entry )
|
|
444
|
-
failing << entry
|
|
445
|
-
elsif default_branch_check_run_pending?( entry: entry )
|
|
446
|
-
pending << entry
|
|
447
|
-
end
|
|
448
|
-
end
|
|
449
|
-
[ failing, pending ]
|
|
450
|
-
end
|
|
451
|
-
|
|
452
|
-
# Separates check-run entries into critical and advisory buckets based on configured advisory names.
|
|
453
|
-
def separate_advisory_check_entries( entries:, advisory_names: )
|
|
454
|
-
advisory, critical = Array( entries ).partition do |entry|
|
|
455
|
-
advisory_names.include?( entry[ "name" ].to_s.strip )
|
|
456
|
-
end
|
|
457
|
-
[ critical, advisory ]
|
|
458
|
-
end
|
|
459
|
-
|
|
460
|
-
# Returns true when a required-check entry is in a non-passing, non-pending state.
|
|
461
|
-
# Cancelled, errored, timed-output, and any unknown bucket all count as failing.
|
|
462
|
-
def check_entry_failing?( entry: )
|
|
463
|
-
!%w[pass pending].include?( entry[ "bucket" ].to_s )
|
|
464
|
-
end
|
|
465
|
-
|
|
466
|
-
# Failing means completed with a non-successful conclusion.
|
|
467
|
-
def default_branch_check_run_failing?( entry: )
|
|
468
|
-
status = entry[ "status" ].to_s.strip.downcase
|
|
469
|
-
conclusion = entry[ "conclusion" ].to_s.strip.downcase
|
|
470
|
-
status == "completed" && !conclusion.empty? && !%w[success neutral skipped].include?( conclusion )
|
|
471
|
-
end
|
|
472
|
-
|
|
473
|
-
# Pending includes non-completed checks and completed checks missing conclusion.
|
|
474
|
-
def default_branch_check_run_pending?( entry: )
|
|
475
|
-
status = entry[ "status" ].to_s.strip.downcase
|
|
476
|
-
conclusion = entry[ "conclusion" ].to_s.strip.downcase
|
|
477
|
-
return true if status.empty?
|
|
478
|
-
return true unless status == "completed"
|
|
479
|
-
|
|
480
|
-
conclusion.empty?
|
|
481
|
-
end
|
|
482
|
-
|
|
483
|
-
# Normalises default-branch check-runs to report layout used by markdown output.
|
|
484
|
-
def normalise_default_branch_check_entries( entries: )
|
|
485
|
-
Array( entries ).map do |entry|
|
|
486
|
-
state = if entry[ "status" ].to_s.strip.downcase == "completed"
|
|
487
|
-
blank_to( value: entry[ "conclusion" ], default: "UNKNOWN" )
|
|
488
|
-
else
|
|
489
|
-
blank_to( value: entry[ "status" ], default: "UNKNOWN" )
|
|
490
|
-
end
|
|
491
|
-
{
|
|
492
|
-
workflow: blank_to( value: entry.dig( "app", "name" ), default: "workflow" ),
|
|
493
|
-
name: blank_to( value: entry[ "name" ], default: "check" ),
|
|
494
|
-
state: state.upcase,
|
|
495
|
-
link: entry[ "html_url" ].to_s
|
|
496
|
-
}
|
|
497
|
-
end
|
|
498
|
-
end
|
|
499
|
-
|
|
500
|
-
# Writes monitor report artefacts and prints their locations.
|
|
501
|
-
def write_and_print_pr_monitor_report( report: )
|
|
502
|
-
markdown_path, json_path = write_pr_monitor_report( report: report )
|
|
503
|
-
puts_verbose "report_markdown: #{markdown_path}"
|
|
504
|
-
puts_verbose "report_json: #{json_path}"
|
|
505
|
-
rescue StandardError => exception
|
|
506
|
-
puts_verbose "report_write: SKIP (#{exception.message})"
|
|
507
|
-
end
|
|
508
|
-
|
|
509
|
-
# Persists report in both machine-readable JSON and human-readable Markdown.
|
|
510
|
-
def write_pr_monitor_report( report: )
|
|
511
|
-
report_dir = report_dir_path
|
|
512
|
-
FileUtils.mkdir_p( report_dir )
|
|
513
|
-
markdown_path = File.join( report_dir, REPORT_MD )
|
|
514
|
-
json_path = File.join( report_dir, REPORT_JSON )
|
|
515
|
-
File.write( json_path, JSON.pretty_generate( report ) )
|
|
516
|
-
File.write( markdown_path, render_pr_monitor_markdown( report: report ) )
|
|
517
|
-
[ markdown_path, json_path ]
|
|
518
|
-
end
|
|
519
|
-
|
|
520
|
-
# Renders Markdown summary used by humans during merge-readiness reviews.
|
|
521
|
-
def render_pr_monitor_markdown( report: )
|
|
522
|
-
lines = []
|
|
523
|
-
lines << "# Carson PR Monitor Report"
|
|
524
|
-
lines << ""
|
|
525
|
-
lines << "- Generated at: #{report.fetch( :generated_at )}"
|
|
526
|
-
lines << "- Branch: #{report.fetch( :branch )}"
|
|
527
|
-
lines << "- Audit status: #{report.fetch( :audit_status, 'unknown' )}"
|
|
528
|
-
lines << "- Monitor status: #{report.fetch( :status )}"
|
|
529
|
-
lines << "- Skip reason: #{report.fetch( :skip_reason )}" unless report.fetch( :skip_reason ).nil?
|
|
530
|
-
lines << ""
|
|
531
|
-
lines << "## PR"
|
|
532
|
-
pr = report[ :pr ]
|
|
533
|
-
if pr.nil?
|
|
534
|
-
lines << "- not available"
|
|
535
|
-
else
|
|
536
|
-
lines << "- Number: ##{pr.fetch( :number )}"
|
|
537
|
-
lines << "- Title: #{pr.fetch( :title )}"
|
|
538
|
-
lines << "- URL: #{pr.fetch( :url )}"
|
|
539
|
-
lines << "- State: #{pr.fetch( :state )}"
|
|
540
|
-
lines << "- Review decision: #{pr.fetch( :review_decision )}"
|
|
541
|
-
end
|
|
542
|
-
lines << ""
|
|
543
|
-
lines << "## Required Checks"
|
|
544
|
-
checks = report.fetch( :checks )
|
|
545
|
-
lines << "- Status: #{checks.fetch( :status )}"
|
|
546
|
-
lines << "- Skip reason: #{checks.fetch( :skip_reason )}" unless checks.fetch( :skip_reason ).nil?
|
|
547
|
-
lines << "- Total: #{checks.fetch( :required_total )}"
|
|
548
|
-
lines << "- Failing: #{checks.fetch( :failing_count )}"
|
|
549
|
-
lines << "- Pending: #{checks.fetch( :pending_count )}"
|
|
550
|
-
lines << ""
|
|
551
|
-
lines << "### Failing"
|
|
552
|
-
if checks.fetch( :failing ).empty?
|
|
553
|
-
lines << "- none"
|
|
554
|
-
else
|
|
555
|
-
checks.fetch( :failing ).each { |entry| lines << "- #{entry.fetch( :workflow )} / #{entry.fetch( :name )} (#{entry.fetch( :state )}) #{entry.fetch( :link )}".strip }
|
|
556
|
-
end
|
|
557
|
-
lines << ""
|
|
558
|
-
lines << "### Pending"
|
|
559
|
-
if checks.fetch( :pending ).empty?
|
|
560
|
-
lines << "- none"
|
|
561
|
-
else
|
|
562
|
-
checks.fetch( :pending ).each { |entry| lines << "- #{entry.fetch( :workflow )} / #{entry.fetch( :name )} (#{entry.fetch( :state )}) #{entry.fetch( :link )}".strip }
|
|
563
|
-
end
|
|
564
|
-
lines << ""
|
|
565
|
-
lines << "## Default Branch CI Baseline"
|
|
566
|
-
baseline = report[ :default_branch_baseline ]
|
|
567
|
-
if baseline.nil?
|
|
568
|
-
lines << "- not available"
|
|
569
|
-
else
|
|
570
|
-
lines << "- Status: #{baseline.fetch( :status )}"
|
|
571
|
-
lines << "- Skip reason: #{baseline.fetch( :skip_reason )}" unless baseline.fetch( :skip_reason ).nil?
|
|
572
|
-
lines << "- Repository: #{baseline.fetch( :repository )}" unless baseline.fetch( :repository ).nil?
|
|
573
|
-
lines << "- Branch: #{baseline.fetch( :default_branch )}" unless baseline.fetch( :default_branch ).nil?
|
|
574
|
-
lines << "- Head SHA: #{baseline.fetch( :head_sha )}" unless baseline.fetch( :head_sha ).nil?
|
|
575
|
-
lines << "- Workflow files: #{baseline.fetch( :workflows_total )}"
|
|
576
|
-
lines << "- Check-runs: #{baseline.fetch( :check_runs_total )}"
|
|
577
|
-
lines << "- Failing: #{baseline.fetch( :failing_count )}"
|
|
578
|
-
lines << "- Pending: #{baseline.fetch( :pending_count )}"
|
|
579
|
-
lines << "- No check evidence: #{baseline.fetch( :no_check_evidence )}"
|
|
580
|
-
lines << ""
|
|
581
|
-
lines << "### Baseline Failing"
|
|
582
|
-
if baseline.fetch( :failing ).empty?
|
|
583
|
-
lines << "- none"
|
|
584
|
-
else
|
|
585
|
-
baseline.fetch( :failing ).each { |entry| lines << "- #{entry.fetch( :workflow )} / #{entry.fetch( :name )} (#{entry.fetch( :state )}) #{entry.fetch( :link )}".strip }
|
|
586
|
-
end
|
|
587
|
-
lines << ""
|
|
588
|
-
lines << "### Baseline Pending"
|
|
589
|
-
if baseline.fetch( :pending ).empty?
|
|
590
|
-
lines << "- none"
|
|
591
|
-
else
|
|
592
|
-
baseline.fetch( :pending ).each { |entry| lines << "- #{entry.fetch( :workflow )} / #{entry.fetch( :name )} (#{entry.fetch( :state )}) #{entry.fetch( :link )}".strip }
|
|
593
|
-
end
|
|
594
|
-
end
|
|
595
|
-
lines << ""
|
|
596
|
-
lines.join( "\n" )
|
|
597
|
-
end
|
|
598
|
-
|
|
599
|
-
end
|
|
600
|
-
|
|
601
|
-
include Audit
|
|
602
|
-
end
|
|
603
|
-
end
|