Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ Notable changes to JevGate. Versions follow [Semantic Versioning](https://semver

## [Unreleased]

- `--format gitlab` writes a GitLab Code Quality report, so merge requests show the findings: the ones `--format github` annotates, `major` when they fail the gate and `minor` otherwise, with JevGate's fingerprints.
- `jevgate.schema.json` is a JSON Schema of `jevgate.toml`, generated from the configuration types with every rule name and level, and `jevgate init` writes a `#:schema` line so editors with TOML schema support complete and check the file.
- pre-commit hooks: `jevgate-system` runs the installed `jevgate` on the staged changes, and `jevgate` builds it from source with Rust first.
- `--format sarif` writes a SARIF 2.1.0 log for GitHub code scanning, GitLab and editors: the findings `--format github` annotates, as `error` when they fail the gate and `warning` otherwise, with every rule's question, related locations, the finding's fingerprint and probability. Run errors and files that could not be judged are tool notifications.
Expand Down
21 changes: 21 additions & 0 deletions site/src/ci.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,25 @@ repos:
- id: jevgate-system # the jevgate on PATH; `jevgate` builds it with Rust instead
```

On GitLab, a merge request pipeline can show the findings in the merge request with a Code Quality report. Set `TYPESAFE_API_KEY` as a masked CI/CD variable:

```yaml
jevgate:
image: buildpack-deps:bookworm-scm # any image with git, curl and tar
variables:
GIT_DEPTH: 0 # --base compares with the fork point
cache:
key: jevgate-answers
paths: [.jevgate/cache]
script:
- curl -fsSL https://raw.githubusercontent.com/Tech-Byte-Frontier/jevgate/main/install.sh | sh
- ~/.local/bin/jevgate check --base "$CI_MERGE_REQUEST_DIFF_BASE_SHA" --format gitlab > gl-code-quality-report.json
artifacts:
when: always
reports:
codequality: gl-code-quality-report.json
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
```

Other CI systems work the same way: install with `install.sh` or `cargo binstall`, set `TYPESAFE_API_KEY`, keep `.jevgate/cache` between runs, and read the exit code or the JSON report.
1 change: 1 addition & 0 deletions site/src/output.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
| `jsonl` | One compact report per line; one per evaluation with `--watch` |
| `github` | GitHub Actions annotations and job summary, then the agent text |
| `sarif` | A SARIF 2.1.0 log for [GitHub code scanning](https://docs.github.com/en/code-security/code-scanning/integrating-with-code-scanning/uploading-a-sarif-file-to-github) and other SARIF readers: the findings the annotations show, `error` when they fail the gate |
| `gitlab` | A [GitLab Code Quality](https://docs.gitlab.com/ci/testing/code_quality/) report for merge requests: the same findings, `major` when they fail the gate and `minor` otherwise |

Agent output is colored on a terminal; `--color never`, or `NO_COLOR` set to any value, turns it off, and `--color always` or `CLICOLOR_FORCE` turns it on for pipes and logs.

Expand Down
2 changes: 1 addition & 1 deletion src/check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ fn validate(args: &CheckArgs) -> Result<()> {
!(args.watch
&& matches!(
args.output_format(),
Format::Json | Format::Github | Format::Sarif
Format::Json | Format::Github | Format::Sarif | Format::Gitlab
)),
"Use --format jsonl for watch snapshots"
);
Expand Down
84 changes: 84 additions & 0 deletions src/gitlab.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
//! `--format gitlab`: the findings as a GitLab Code Quality report, which
//! merge requests show as a widget and on the changed lines.
use crate::{
options::CheckArgs,
output,
schema::{Finding, Report, Strength},
};
use anyhow::Result;
use serde_json::{Value, json};
use std::{io::Write, path::Path};

/// The findings the GitHub annotations show: `major` when a finding fails the
/// gate, `minor` otherwise.
pub fn emit(out: &mut impl Write, report: &Report, args: &CheckArgs) -> Result<()> {
let issues: Vec<Value> = output::ranked(report)
.into_iter()
.filter(|(_, f)| f.strength != Strength::Note && !f.baselined)
.map(|(path, finding)| issue(path, finding, crate::gate::fails(finding, path, args)))
.collect();
serde_json::to_writer_pretty(&mut *out, &issues)?;
writeln!(out)?;
Ok(())
}

fn issue(path: &Path, finding: &Finding, fails: bool) -> Value {
let end = finding
.locations
.iter()
.find(|l| l.path == path && l.start_line == finding.line)
.map_or(finding.line, |l| l.end_line.max(finding.line));
json!({
"description": format!("{} Next step: {}", finding.message, finding.action),
"check_name": finding.rule,
"fingerprint": fingerprint(path, finding),
"severity": if fails { "major" } else { "minor" },
"location": {
"path": path.to_string_lossy(),
"lines": {"begin": finding.line.max(1), "end": end.max(1)},
},
})
}

/// GitLab requires a fingerprint per issue; findings carry JevGate's, and one
/// without is named by its rule and place.
fn fingerprint(path: &Path, finding: &Finding) -> String {
if !finding.fingerprint.is_empty() {
return finding.fingerprint.clone();
}
let place = [
finding.rule.clone(),
path.display().to_string(),
finding.line.to_string(),
]
.join(crate::schema::HASH_SEPARATOR);
crate::schema::hash(place.as_bytes())
}

#[cfg(test)]
mod tests {
use super::*;
use crate::tests::finding;

#[test]
fn issues_carry_severity_location_and_a_fingerprint() {
let path = Path::new("src/a,b.rs");
let review = issue(path, &finding(Strength::Review), true);
assert_eq!(review["severity"], "major");
assert_eq!(review["check_name"], "maintainability/shared-logic");
assert_eq!(review["location"]["path"], "src/a,b.rs");
assert_eq!(review["location"]["lines"], json!({"begin": 12, "end": 20}));
assert!(
review["description"]
.as_str()
.unwrap()
.ends_with("Next step: Share one | implementation")
);
assert_eq!(review["fingerprint"].as_str().unwrap().len(), 64);
let consider = issue(path, &finding(Strength::Consider), false);
assert_eq!(consider["severity"], "minor");
let mut named = finding(Strength::Consider);
named.fingerprint = "abc".into();
assert_eq!(issue(path, &named, false)["fingerprint"], "abc");
}
}
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ mod evaluate;
mod file_kind;
mod gate;
mod github;
mod gitlab;
mod html_report;
mod init;
mod inventory;
Expand Down
2 changes: 2 additions & 0 deletions src/options/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ pub enum Format {
Github,
/// A SARIF 2.1.0 log, for GitHub code scanning and other SARIF readers
Sarif,
/// A GitLab Code Quality report, for merge request widgets
Gitlab,
}

/// When agent output is colored.
Expand Down
1 change: 1 addition & 0 deletions src/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ pub fn emit(report: &Report, args: &CheckArgs) -> Result<()> {
),
Format::Github => crate::github::emit(&mut out, report, args),
Format::Sarif => crate::sarif::emit(&mut out, report, args),
Format::Gitlab => crate::gitlab::emit(&mut out, report, args),
};
match written {
Err(error) if broken_pipe(&error) => Ok(()),
Expand Down
Loading