Skip to content

Commit cc6f84a

Browse files
authored
Merge pull request #10 from thinkgrid-labs/dev
Dev
2 parents e6459c4 + 16a3860 commit cc6f84a

19 files changed

Lines changed: 1869 additions & 1973 deletions

File tree

.github/workflows/ci.yml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,33 @@ jobs:
160160
if: github.event_name == 'pull_request' && hashFiles('.greengate-baseline.json') != ''
161161
run: ./target/release/greengate scan --since-baseline
162162

163+
# ── PR review — Complexity Score + new-code coverage gaps ──────────────────
164+
# Outputs a Complexity Score (estimated review effort) and flags newly added
165+
# lines that are not covered by tests. Only runs on pull_request events so
166+
# there is always a HEAD~1 to diff against.
167+
- name: PR review (complexity + coverage gaps)
168+
if: github.event_name == 'pull_request'
169+
env:
170+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
171+
GITHUB_REPOSITORY: ${{ github.repository }}
172+
GITHUB_SHA: ${{ github.sha }}
173+
run: |
174+
./target/release/greengate review \
175+
--base "${{ github.event.pull_request.base.sha }}" \
176+
--format json \
177+
--annotate \
178+
> review-report.json 2>&1 || true
179+
cat review-report.json
180+
continue-on-error: true # informational until coverage reports are wired in
181+
182+
- name: Upload PR review report
183+
if: github.event_name == 'pull_request'
184+
uses: actions/upload-artifact@v4
185+
with:
186+
name: review-report
187+
path: review-report.json
188+
continue-on-error: true
189+
163190
# ── Kubernetes lint — exits 0 when no YAML manifests are present ─────────
164191
- name: Lint Kubernetes manifests
165192
run: ./target/release/greengate lint

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "greengate"
3-
version = "0.2.11"
3+
version = "0.2.12"
44
edition = "2024"
55

66
[dependencies]

README.md

Lines changed: 81 additions & 1962 deletions
Large diffs are not rendered by default.

docs/commands/review.md

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
# review — PR Diff Analyzer
2+
3+
Analyzes a pull request diff and outputs two things:
4+
5+
1. **Complexity Score** — a weighted composite that estimates how long the PR will take to review, derived from lines changed, files touched, and cyclomatic complexity of new code (detected via tree-sitter AST queries on JS/TS/Python/Go).
6+
2. **New-code coverage gaps** — which newly added lines are NOT covered by the provided LCOV or Cobertura report, with a pass/fail gate against a configurable floor.
7+
8+
Both outputs are available on every commit, not just in CI — making this practical as a local pre-push check or a `greengate run` pipeline step.
9+
10+
## Usage
11+
12+
```
13+
greengate review [OPTIONS]
14+
15+
Options:
16+
--base <REF> Diff base: commit, branch, or tag [default: HEAD~1]
17+
--staged Diff staged changes instead of committed diff
18+
--coverage-file <FILE> LCOV or Cobertura XML coverage file to cross-reference
19+
--min-coverage <PCT> Minimum % coverage required for newly added lines [default: 80]
20+
--complexity-budget <N> Fail if Complexity Score exceeds this; 0 = warn only [default: 0]
21+
--format <FMT> Output format: text | json | sarif [default: text]
22+
--annotate Post results as a GitHub Check Run + PR comment
23+
-h, --help Print help
24+
```
25+
26+
## Complexity Score
27+
28+
The score is computed on the **added lines only** (not deleted or context lines):
29+
30+
```
31+
raw = (lines_added × 0.3)
32+
+ (files_changed × 5.0)
33+
+ (cyclomatic_nodes × 2.0)
34+
+ (lines_removed × 0.1)
35+
36+
score = round(raw)
37+
estimated_review_minutes = min(raw × 0.5, 120)
38+
```
39+
40+
**Tier labels:**
41+
42+
| Score | Label | Estimated review time |
43+
|---|---|---|
44+
| 0 – 20 | Quick Review | < 10 min |
45+
| 21 – 50 | Normal Review | 10 – 25 min |
46+
| 51 – 100 | Complex Review | 25 – 50 min |
47+
| 101+ | Large PR — consider splitting | > 50 min |
48+
49+
**Cyclomatic complexity** is counted by running tree-sitter queries over the added code:
50+
- JS/TS/TSX/JSX: `if`, ternary, `switch case`, `for`, `while`, `do`, `catch`
51+
- Python: `if`, `elif`, `for`, `while`, `except`, boolean operators, conditional expressions
52+
- Go: `if`, `for`, `switch`, `select`, `binary_expression`
53+
- Other file types: 0 (not counted, but lines/files still contribute to the score)
54+
55+
## New-code coverage gaps
56+
57+
When `--coverage-file` is provided, greengate cross-references the diff with the coverage report line by line:
58+
59+
| Line state | Outcome |
60+
|---|---|
61+
| In coverage report with `hits > 0` | **covered** — counts toward % |
62+
| In coverage report with `hits == 0` | **uncovered** — gap, counts against % |
63+
| Not in coverage report | **unmeasured** — excluded from % calculation (no penalty) |
64+
65+
Per-file and overall `new_code_coverage_pct` are computed from covered + uncovered lines only. The gate fails if `overall_pct < min_coverage`.
66+
67+
## Examples
68+
69+
```bash
70+
# Complexity score only (no coverage gate)
71+
greengate review --base main
72+
73+
# With coverage gate — fails if new-code coverage < 80%
74+
greengate review --base main --coverage-file coverage/lcov.info --min-coverage 80
75+
76+
# Staged changes (before committing)
77+
greengate review --staged --coverage-file coverage/lcov.info
78+
79+
# JSON output (machine-readable)
80+
greengate review --base HEAD~1 --coverage-file coverage/lcov.info --format json
81+
82+
# SARIF output — upload uncovered lines to GitHub Security tab
83+
greengate review --base HEAD~1 --coverage-file coverage/lcov.info --format sarif > review.sarif
84+
85+
# Set a hard complexity budget (fail if score > 100)
86+
greengate review --base main --complexity-budget 100
87+
88+
# GitHub Actions — annotate PR with uncovered lines + score
89+
greengate review \
90+
--base "${{ github.event.pull_request.base.sha }}" \
91+
--coverage-file coverage/lcov.info \
92+
--min-coverage 80 \
93+
--annotate
94+
```
95+
96+
## Sample output (text)
97+
98+
```
99+
╔══ PR Review ═══════════════════════════════════════╗
100+
Complexity Score : 47 (Normal Review, ~23 min)
101+
Files changed : 5
102+
Lines added/del : +120 / -34
103+
Cyclomatic nodes : 18
104+
╚════════════════════════════════════════════════════╝
105+
106+
New-Code Coverage: 73.3% ✗ (target: 80.0%)
107+
108+
src/engine.rs 12/15 added lines covered (80.0%) ✓
109+
src/scanner.rs 6/11 added lines covered (54.5%) ✗
110+
Uncovered lines: 88, 89, 92, 95, 101
111+
112+
⚠️ Review gate FAILED.
113+
Error: greengate review: quality gate failed.
114+
```
115+
116+
## JSON output schema
117+
118+
```json
119+
{
120+
"complexity": {
121+
"score": 47,
122+
"tier": "Normal Review",
123+
"estimated_review_minutes": 23,
124+
"files_changed": 5,
125+
"lines_added": 120,
126+
"lines_removed": 34,
127+
"cyclomatic_nodes": 18
128+
},
129+
"coverage": {
130+
"overall_pct": 73.3,
131+
"min_required": 80.0,
132+
"passed": false,
133+
"files": [
134+
{
135+
"file": "src/scanner.rs",
136+
"added_lines": 11,
137+
"covered": 6,
138+
"uncovered": 5,
139+
"unmeasured": 0,
140+
"coverage_pct": 54.5,
141+
"uncovered_lines": [88, 89, 92, 95, 101]
142+
}
143+
]
144+
},
145+
"passed": false
146+
}
147+
```
148+
149+
## Configuration
150+
151+
Configure defaults in `.greengate.toml`:
152+
153+
```toml
154+
[review]
155+
min_new_code_coverage = 80 # minimum % for newly added lines (default: 80)
156+
complexity_budget = 0 # 0 = warn only; > 0 = fail threshold (default: 0)
157+
```
158+
159+
CLI flags always override config values.
160+
161+
## GitHub integration (`--annotate`)
162+
163+
When run with `--annotate` and the `GITHUB_TOKEN`, `GITHUB_REPOSITORY`, and `GITHUB_SHA` environment variables present, `review` will:
164+
165+
1. Create a **GitHub Check Run** named `greengate review` with the Complexity Score as the title
166+
2. Post a **per-line annotation** (warning level) for each uncovered added line
167+
3. Post a **PR comment** with a markdown summary table
168+
169+
If any of the required environment variables are absent, `--annotate` is a no-op.
170+
171+
## Pipeline usage
172+
173+
```toml
174+
[pipeline]
175+
steps = [
176+
"scan",
177+
"review --base main --coverage-file coverage/lcov.info --min-coverage 80",
178+
"coverage --min 80",
179+
"audit",
180+
]
181+
```
182+
183+
## GitHub Actions
184+
185+
```yaml
186+
- name: PR review (complexity + coverage gaps)
187+
if: github.event_name == 'pull_request'
188+
env:
189+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
190+
GITHUB_REPOSITORY: ${{ github.repository }}
191+
GITHUB_SHA: ${{ github.sha }}
192+
run: |
193+
greengate review \
194+
--base "${{ github.event.pull_request.base.sha }}" \
195+
--coverage-file coverage/lcov.info \
196+
--min-coverage 80 \
197+
--annotate
198+
```
199+
200+
Required permissions:
201+
```yaml
202+
permissions:
203+
checks: write # create Check Runs
204+
pull-requests: write # post PR comments
205+
```
206+
207+
## Exit codes
208+
209+
| Code | Meaning |
210+
|---|---|
211+
| `0` | All gates passed (or no coverage file specified) |
212+
| `1` | Coverage below threshold, complexity budget exceeded, or a runtime error |
213+
214+
## Notes
215+
216+
- Coverage gaps are computed only for **measurable** lines — lines not present in the coverage report at all (e.g. blank lines, comments, or untested new files) are treated as **unmeasured** and excluded from the percentage calculation rather than counted as failures
217+
- The complexity scoring runs tree-sitter on **added lines only** (not the full file). This means the score reflects only the new code being introduced, not background complexity in the file
218+
- For non-git directories or when the diff is empty, the command exits 0 with a brief message
219+
- SARIF output from `review` uses rule ID `GG/NewCodeUncovered` and severity `warning`; it can be uploaded to GitHub Advanced Security alongside the `scan` SARIF output

docs/commands/run.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ steps = [
2222
"docker-lint",
2323
"lighthouse",
2424
"reassure",
25+
"review --base HEAD~1",
2526
]
2627
```
2728

@@ -38,6 +39,7 @@ Each step is a string that maps to an `greengate` subcommand, optionally followe
3839
| `audit` | `greengate audit` |
3940
| `lighthouse` | `greengate lighthouse` |
4041
| `reassure` | `greengate reassure` |
42+
| `review` | `greengate review` |
4143

4244
## Step flags
4345

docs/guide/ci-integration.md

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ jobs:
1818
runs-on: ubuntu-latest
1919
steps:
2020
- uses: actions/checkout@v4
21+
with:
22+
fetch-depth: 0 # full history required for --base diff
2123

2224
- name: Install GreenGate
2325
run: |
@@ -30,6 +32,20 @@ jobs:
3032
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
3133
run: greengate scan --annotate
3234

35+
- name: PR Review (Complexity + Coverage Gaps)
36+
if: github.event_name == 'pull_request'
37+
env:
38+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
39+
GITHUB_REPOSITORY: ${{ github.repository }}
40+
GITHUB_SHA: ${{ github.sha }}
41+
run: |
42+
greengate review \
43+
--base "${{ github.event.pull_request.base.sha }}" \
44+
--coverage-file coverage/lcov.info \
45+
--min-coverage 80 \
46+
--annotate
47+
continue-on-error: true # informational until coverage is wired in
48+
3349
- name: Kubernetes Lint
3450
run: greengate lint --dir ./k8s
3551

@@ -62,33 +78,45 @@ stages:
6278
- security
6379
- quality
6480
65-
.install_oxide: &install_oxide
81+
.install_greengate: &install_greengate
6682
before_script:
6783
- curl -sL https://github.com/thinkgrid-labs/greengate/releases/latest/download/greengate-linux-amd64
6884
-o /usr/local/bin/greengate
6985
- chmod +x /usr/local/bin/greengate
7086
7187
secret-scan:
7288
stage: security
73-
<<: *install_oxide
89+
<<: *install_greengate
7490
script:
7591
- greengate scan
7692
93+
pr-review:
94+
stage: quality
95+
<<: *install_greengate
96+
only:
97+
- merge_requests
98+
script:
99+
- greengate review
100+
--base "$CI_MERGE_REQUEST_DIFF_BASE_SHA"
101+
--coverage-file coverage/lcov.info
102+
--min-coverage 80
103+
allow_failure: true # informational until coverage is wired in
104+
77105
k8s-lint:
78106
stage: security
79-
<<: *install_oxide
107+
<<: *install_greengate
80108
script:
81109
- greengate lint --dir ./k8s
82110
83111
coverage-gate:
84112
stage: quality
85-
<<: *install_oxide
113+
<<: *install_greengate
86114
script:
87115
- greengate coverage --file coverage/lcov.info --min 80
88116
89117
dependency-audit:
90118
stage: security
91-
<<: *install_oxide
119+
<<: *install_greengate
92120
script:
93121
- greengate audit
94122
```

docs/guide/getting-started.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ greengate --version
5050
# Scan for secrets and run SAST on JS/TS files
5151
greengate scan
5252

53+
# Analyze a PR: Complexity Score + new-code coverage gaps
54+
greengate review --base main --coverage-file coverage/lcov.info
55+
5356
# Lint all Kubernetes YAML files
5457
greengate lint --dir ./k8s
5558

@@ -75,6 +78,8 @@ greengate reassure
7578
|---|---|
7679
| Hardcoded secrets pushed to git | `greengate scan` |
7780
| XSS, eval, command injection in JS/TS | `greengate scan` (SAST) |
81+
| PR too complex — hard to estimate review time | `greengate review` |
82+
| New code added without test coverage | `greengate review --coverage-file lcov.info` |
7883
| Kubernetes manifests missing resource limits | `greengate lint` |
7984
| Test coverage silently dropping | `greengate coverage` |
8085
| Vulnerable dependencies shipping to production | `greengate audit` |
@@ -87,3 +92,4 @@ greengate reassure
8792
- Set up a [configuration file](/reference/config) to share settings across all commands
8893
- Integrate with [GitHub Actions or GitLab CI](/guide/ci-integration)
8994
- Explore individual [command references](/commands/scan)
95+
- See [PR review](/commands/review) for the `review` subcommand

0 commit comments

Comments
 (0)