From 5c383b4971c081ffa4025a5fbc73f1ba1e38c500 Mon Sep 17 00:00:00 2001 From: Tauan BF <11513929+tauanbinato@users.noreply.github.com> Date: Fri, 25 Sep 2026 19:55:42 -0300 Subject: [PATCH] Write findings as a GitLab Code Quality report --format gitlab prints the findings --format github annotates as Code Quality issues, major when they fail the gate and minor otherwise, with JevGate's fingerprints. The site shows a merge request pipeline that uploads it. --- CHANGELOG.md | 1 + site/src/ci.md | 21 ++++++++++++ site/src/output.md | 1 + src/check.rs | 2 +- src/gitlab.rs | 84 ++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 1 + src/options/mod.rs | 2 ++ src/output.rs | 1 + 8 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 src/gitlab.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 9568ca4..b5a9c03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/site/src/ci.md b/site/src/ci.md index ab31710..19660f2 100644 --- a/site/src/ci.md +++ b/site/src/ci.md @@ -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. diff --git a/site/src/output.md b/site/src/output.md index e2a6fbe..aff6aa2 100644 --- a/site/src/output.md +++ b/site/src/output.md @@ -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. diff --git a/src/check.rs b/src/check.rs index e750593..4a228fa 100644 --- a/src/check.rs +++ b/src/check.rs @@ -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" ); diff --git a/src/gitlab.rs b/src/gitlab.rs new file mode 100644 index 0000000..764832f --- /dev/null +++ b/src/gitlab.rs @@ -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 = 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"); + } +} diff --git a/src/main.rs b/src/main.rs index 178fb95..98db43a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -36,6 +36,7 @@ mod evaluate; mod file_kind; mod gate; mod github; +mod gitlab; mod html_report; mod init; mod inventory; diff --git a/src/options/mod.rs b/src/options/mod.rs index a1d89e2..f1c68fb 100644 --- a/src/options/mod.rs +++ b/src/options/mod.rs @@ -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. diff --git a/src/output.rs b/src/output.rs index 194651d..aa3377d 100644 --- a/src/output.rs +++ b/src/output.rs @@ -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(()),