Skip to content
Merged

Dev #10

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,33 @@ jobs:
if: github.event_name == 'pull_request' && hashFiles('.greengate-baseline.json') != ''
run: ./target/release/greengate scan --since-baseline

# ── PR review — Complexity Score + new-code coverage gaps ──────────────────
# Outputs a Complexity Score (estimated review effort) and flags newly added
# lines that are not covered by tests. Only runs on pull_request events so
# there is always a HEAD~1 to diff against.
- name: PR review (complexity + coverage gaps)
if: github.event_name == 'pull_request'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_REPOSITORY: ${{ github.repository }}
GITHUB_SHA: ${{ github.sha }}
run: |
./target/release/greengate review \
--base "${{ github.event.pull_request.base.sha }}" \
--format json \
--annotate \
> review-report.json 2>&1 || true
cat review-report.json
continue-on-error: true # informational until coverage reports are wired in

- name: Upload PR review report
if: github.event_name == 'pull_request'
uses: actions/upload-artifact@v4
with:
name: review-report
path: review-report.json
continue-on-error: true

# ── Kubernetes lint — exits 0 when no YAML manifests are present ─────────
- name: Lint Kubernetes manifests
run: ./target/release/greengate lint
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "greengate"
version = "0.2.11"
version = "0.2.12"
edition = "2024"

[dependencies]
Expand Down
2,043 changes: 81 additions & 1,962 deletions README.md

Large diffs are not rendered by default.

219 changes: 219 additions & 0 deletions docs/commands/review.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
# review — PR Diff Analyzer

Analyzes a pull request diff and outputs two things:

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).
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.

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.

## Usage

```
greengate review [OPTIONS]

Options:
--base <REF> Diff base: commit, branch, or tag [default: HEAD~1]
--staged Diff staged changes instead of committed diff
--coverage-file <FILE> LCOV or Cobertura XML coverage file to cross-reference
--min-coverage <PCT> Minimum % coverage required for newly added lines [default: 80]
--complexity-budget <N> Fail if Complexity Score exceeds this; 0 = warn only [default: 0]
--format <FMT> Output format: text | json | sarif [default: text]
--annotate Post results as a GitHub Check Run + PR comment
-h, --help Print help
```

## Complexity Score

The score is computed on the **added lines only** (not deleted or context lines):

```
raw = (lines_added × 0.3)
+ (files_changed × 5.0)
+ (cyclomatic_nodes × 2.0)
+ (lines_removed × 0.1)

score = round(raw)
estimated_review_minutes = min(raw × 0.5, 120)
```

**Tier labels:**

| Score | Label | Estimated review time |
|---|---|---|
| 0 – 20 | Quick Review | < 10 min |
| 21 – 50 | Normal Review | 10 – 25 min |
| 51 – 100 | Complex Review | 25 – 50 min |
| 101+ | Large PR — consider splitting | > 50 min |

**Cyclomatic complexity** is counted by running tree-sitter queries over the added code:
- JS/TS/TSX/JSX: `if`, ternary, `switch case`, `for`, `while`, `do`, `catch`
- Python: `if`, `elif`, `for`, `while`, `except`, boolean operators, conditional expressions
- Go: `if`, `for`, `switch`, `select`, `binary_expression`
- Other file types: 0 (not counted, but lines/files still contribute to the score)

## New-code coverage gaps

When `--coverage-file` is provided, greengate cross-references the diff with the coverage report line by line:

| Line state | Outcome |
|---|---|
| In coverage report with `hits > 0` | **covered** — counts toward % |
| In coverage report with `hits == 0` | **uncovered** — gap, counts against % |
| Not in coverage report | **unmeasured** — excluded from % calculation (no penalty) |

Per-file and overall `new_code_coverage_pct` are computed from covered + uncovered lines only. The gate fails if `overall_pct < min_coverage`.

## Examples

```bash
# Complexity score only (no coverage gate)
greengate review --base main

# With coverage gate — fails if new-code coverage < 80%
greengate review --base main --coverage-file coverage/lcov.info --min-coverage 80

# Staged changes (before committing)
greengate review --staged --coverage-file coverage/lcov.info

# JSON output (machine-readable)
greengate review --base HEAD~1 --coverage-file coverage/lcov.info --format json

# SARIF output — upload uncovered lines to GitHub Security tab
greengate review --base HEAD~1 --coverage-file coverage/lcov.info --format sarif > review.sarif

# Set a hard complexity budget (fail if score > 100)
greengate review --base main --complexity-budget 100

# GitHub Actions — annotate PR with uncovered lines + score
greengate review \
--base "${{ github.event.pull_request.base.sha }}" \
--coverage-file coverage/lcov.info \
--min-coverage 80 \
--annotate
```

## Sample output (text)

```
╔══ PR Review ═══════════════════════════════════════╗
Complexity Score : 47 (Normal Review, ~23 min)
Files changed : 5
Lines added/del : +120 / -34
Cyclomatic nodes : 18
╚════════════════════════════════════════════════════╝

New-Code Coverage: 73.3% ✗ (target: 80.0%)

src/engine.rs 12/15 added lines covered (80.0%) ✓
src/scanner.rs 6/11 added lines covered (54.5%) ✗
Uncovered lines: 88, 89, 92, 95, 101

⚠️ Review gate FAILED.
Error: greengate review: quality gate failed.
```

## JSON output schema

```json
{
"complexity": {
"score": 47,
"tier": "Normal Review",
"estimated_review_minutes": 23,
"files_changed": 5,
"lines_added": 120,
"lines_removed": 34,
"cyclomatic_nodes": 18
},
"coverage": {
"overall_pct": 73.3,
"min_required": 80.0,
"passed": false,
"files": [
{
"file": "src/scanner.rs",
"added_lines": 11,
"covered": 6,
"uncovered": 5,
"unmeasured": 0,
"coverage_pct": 54.5,
"uncovered_lines": [88, 89, 92, 95, 101]
}
]
},
"passed": false
}
```

## Configuration

Configure defaults in `.greengate.toml`:

```toml
[review]
min_new_code_coverage = 80 # minimum % for newly added lines (default: 80)
complexity_budget = 0 # 0 = warn only; > 0 = fail threshold (default: 0)
```

CLI flags always override config values.

## GitHub integration (`--annotate`)

When run with `--annotate` and the `GITHUB_TOKEN`, `GITHUB_REPOSITORY`, and `GITHUB_SHA` environment variables present, `review` will:

1. Create a **GitHub Check Run** named `greengate review` with the Complexity Score as the title
2. Post a **per-line annotation** (warning level) for each uncovered added line
3. Post a **PR comment** with a markdown summary table

If any of the required environment variables are absent, `--annotate` is a no-op.

## Pipeline usage

```toml
[pipeline]
steps = [
"scan",
"review --base main --coverage-file coverage/lcov.info --min-coverage 80",
"coverage --min 80",
"audit",
]
```

## GitHub Actions

```yaml
- name: PR review (complexity + coverage gaps)
if: github.event_name == 'pull_request'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_REPOSITORY: ${{ github.repository }}
GITHUB_SHA: ${{ github.sha }}
run: |
greengate review \
--base "${{ github.event.pull_request.base.sha }}" \
--coverage-file coverage/lcov.info \
--min-coverage 80 \
--annotate
```

Required permissions:
```yaml
permissions:
checks: write # create Check Runs
pull-requests: write # post PR comments
```

## Exit codes

| Code | Meaning |
|---|---|
| `0` | All gates passed (or no coverage file specified) |
| `1` | Coverage below threshold, complexity budget exceeded, or a runtime error |

## Notes

- 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
- 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
- For non-git directories or when the diff is empty, the command exits 0 with a brief message
- 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
2 changes: 2 additions & 0 deletions docs/commands/run.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ steps = [
"docker-lint",
"lighthouse",
"reassure",
"review --base HEAD~1",
]
```

Expand All @@ -38,6 +39,7 @@ Each step is a string that maps to an `greengate` subcommand, optionally followe
| `audit` | `greengate audit` |
| `lighthouse` | `greengate lighthouse` |
| `reassure` | `greengate reassure` |
| `review` | `greengate review` |

## Step flags

Expand Down
38 changes: 33 additions & 5 deletions docs/guide/ci-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # full history required for --base diff

- name: Install GreenGate
run: |
Expand All @@ -30,6 +32,20 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: greengate scan --annotate

- name: PR Review (Complexity + Coverage Gaps)
if: github.event_name == 'pull_request'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_REPOSITORY: ${{ github.repository }}
GITHUB_SHA: ${{ github.sha }}
run: |
greengate review \
--base "${{ github.event.pull_request.base.sha }}" \
--coverage-file coverage/lcov.info \
--min-coverage 80 \
--annotate
continue-on-error: true # informational until coverage is wired in

- name: Kubernetes Lint
run: greengate lint --dir ./k8s

Expand Down Expand Up @@ -62,33 +78,45 @@ stages:
- security
- quality

.install_oxide: &install_oxide
.install_greengate: &install_greengate
before_script:
- curl -sL https://github.com/thinkgrid-labs/greengate/releases/latest/download/greengate-linux-amd64
-o /usr/local/bin/greengate
- chmod +x /usr/local/bin/greengate

secret-scan:
stage: security
<<: *install_oxide
<<: *install_greengate
script:
- greengate scan

pr-review:
stage: quality
<<: *install_greengate
only:
- merge_requests
script:
- greengate review
--base "$CI_MERGE_REQUEST_DIFF_BASE_SHA"
--coverage-file coverage/lcov.info
--min-coverage 80
allow_failure: true # informational until coverage is wired in

k8s-lint:
stage: security
<<: *install_oxide
<<: *install_greengate
script:
- greengate lint --dir ./k8s

coverage-gate:
stage: quality
<<: *install_oxide
<<: *install_greengate
script:
- greengate coverage --file coverage/lcov.info --min 80

dependency-audit:
stage: security
<<: *install_oxide
<<: *install_greengate
script:
- greengate audit
```
Expand Down
6 changes: 6 additions & 0 deletions docs/guide/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ greengate --version
# Scan for secrets and run SAST on JS/TS files
greengate scan

# Analyze a PR: Complexity Score + new-code coverage gaps
greengate review --base main --coverage-file coverage/lcov.info

# Lint all Kubernetes YAML files
greengate lint --dir ./k8s

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