Skip to content
Open
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
56 changes: 56 additions & 0 deletions docs/api-examples.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# API Examples

Here are some common ways to interact with the `gitlab-lint` API using `curl`.

## Rules

Get all available rules:

```bash
curl http://localhost:3000/api/v1/rules
```

Get details about a specific rule and which projects violate it:

```bash
curl http://localhost:3000/api/v1/rules/without-readme
```

## Projects

List all analyzed projects:

```bash
curl http://localhost:3000/api/v1/projects
```

Search for projects by name:

```bash
curl http://localhost:3000/api/v1/projects?q=my-project
```

Get a specific project with all its violations:

```bash
curl http://localhost:3000/api/v1/projects/123
```

## Levels

See all severity levels:

```bash
curl http://localhost:3000/api/v1/levels
```

## Statistics

Get overall statistics:

```bash
curl http://localhost:3000/api/v1/stats
```

This shows total projects analyzed, violations by severity level, and other metrics from the last collector run.

43 changes: 43 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Architecture

## How It Works

`gitlab-lint` has two main parts: a collector that gathers data from GitLab and runs linting rules, and an API that serves that data.

## Components

### Collector

The collector connects to your GitLab instance, fetches all projects, and runs each registered rule against them. When a rule fails for a project, that violation gets stored in MongoDB.

It's designed to run periodically - you might set up a cron job to run it nightly, for example. This keeps your linting data fresh without manual intervention.

### API

The API is a straightforward REST service built with Echo. It reads from MongoDB and exposes endpoints for querying projects, rules, and statistics. The frontend (or any HTTP client) can consume these endpoints to display linting results.

### Rules System

Rules are the core of the linting logic. Each rule is a Go struct implementing the `Ruler` interface, which makes the system easy to extend. Want to check for something new? Just write a struct with a `Run` method and register it.

The registry pattern means the collector doesn't need to know about individual rules - it just iterates through whatever's registered and runs them all.

### Database

We use MongoDB for storage. The database layer provides a clean abstraction over Mongo operations, so the collector and API don't deal with database specifics directly.

## Data Flow

When the collector runs, it:

1. Authenticates with GitLab
2. Fetches all projects (paginated)
3. Spawns worker goroutines to process projects in parallel
4. For each non-fork project, runs all registered rules
5. Stores violations in MongoDB
6. Saves statistics about the run

The API then reads this data and serves it through REST endpoints. Simple and effective.

See [diagrams](./diagrams.md) for visual representations of these flows.

24 changes: 24 additions & 0 deletions docs/diagrams.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Diagrams

Visual representations of how `gitlab-lint` is structured and how data flows through the system.

## System Architecture

![Architecture](diagrams/architecture.png)

This shows the main pieces and how they connect. The collector pulls from GitLab, runs rules, and stores results in MongoDB. The API reads from Mongo and serves data to clients.

## Collector Flow

![Collector Flow](diagrams/collector-flow.png)

Here's what happens when you run the collector. It authenticates, fetches projects in pages, spawns workers to process them in parallel, and saves everything to the database. Forks are skipped since they're usually duplicates of upstream projects.

## Rule Processing

![Rule Processing](diagrams/rule-processing.png)

This sequence diagram shows how rules get executed. For each project, the collector runs every registered rule. If a rule's `Run` method returns true (meaning the project violates that rule), a violation record goes into the database.

The nice thing about this design is you can add new rules without touching the collector code - just implement the interface and register it.

32 changes: 32 additions & 0 deletions docs/diagrams/architecture.mmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
graph TB
subgraph GitLab
GL[GitLab API]
end

subgraph Collector
C[Collector Worker]
R[Rules Registry]
end

subgraph Database
DB[(MongoDB)]
end

subgraph API
API_SERVER[API Server<br/>Echo Framework]
ENDPOINTS[Endpoints<br/>/projects<br/>/rules<br/>/stats]
end

subgraph Clients
FRONTEND[Frontend React]
CLI[CLI/curl]
end

GL -->|Fetch Projects| C
C -->|Execute Rules| R
C -->|Store Results| DB
DB -->|Read Data| API_SERVER
API_SERVER --> ENDPOINTS
ENDPOINTS -->|JSON| FRONTEND
ENDPOINTS -->|JSON| CLI

Binary file added docs/diagrams/architecture.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 19 additions & 0 deletions docs/diagrams/collector-flow.mmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
flowchart TD
START([Collector Started]) --> AUTH[Authenticate with GitLab API]
AUTH --> FETCH[Fetch Project List]
FETCH --> CHECK{More Projects?}
CHECK -->|Yes| WORKER[Create Worker Goroutine]
WORKER --> LOOP[For Each Project]
LOOP --> FORK{Is Fork?}
FORK -->|Yes| SKIP[Skip Project]
FORK -->|No| RULES[Execute All Rules]
RULES --> STORE[Store Violations]
STORE --> NEXT[Next Project]
NEXT --> LOOP
SKIP --> NEXT
CHECK -->|No| WAIT[Wait for Workers]
WAIT --> SAVE_RULES[Save Rules to DB]
SAVE_RULES --> SAVE_PROJECTS[Save Projects to DB]
SAVE_PROJECTS --> SAVE_STATS[Save Statistics]
SAVE_STATS --> END([Collector Finished])

Binary file added docs/diagrams/collector-flow.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
24 changes: 24 additions & 0 deletions docs/diagrams/rule-processing.mmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
sequenceDiagram
participant C as Collector
participant R as Rules Registry
participant Rule as Rule (Ruler)
participant GL as GitLab Client
participant P as Project
participant DB as MongoDB

C->>R: GetAllRules()
R-->>C: []Ruler
loop For Each Project
C->>GL: GetProject(id)
GL-->>C: Project
loop For Each Rule
C->>Rule: Run(client, project)
Rule->>P: Check Condition
P-->>Rule: Result
Rule-->>C: bool (violates?)
alt Violation Detected
C->>DB: InsertViolation(rule, project)
end
end
end

Binary file added docs/diagrams/rule-processing.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
135 changes: 135 additions & 0 deletions docs/tutorial-creating-rules.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# Creating a New Rule

This guide walks through adding a new linting rule to `gitlab-lint`. We'll build a rule called `WithoutLicense` that checks whether projects have a license file.

## Understanding Rules

Rules in `gitlab-lint` implement the `Ruler` interface:

```go
type Ruler interface {
Run(client *gitlab.Client, p *gitlab.Project) bool
GetSlug() string
GetLevel() string
}
```

The `Run` method contains your rule's logic and returns `true` when a project violates the rule. `GetSlug` provides a unique identifier, and `GetLevel` sets the severity.

## Writing the Rule

Create `rules/without_license.go`:

```go
// Copyright (c) 2021, Marcelo Jorge Vieira
// Licensed under the BSD 3-Clause License

package rules

import "github.com/xanzy/go-gitlab"

type WithoutLicense struct {
Description string `json:"description"`
ID string `json:"ruleId"`
Level string `json:"level"`
Name string `json:"name"`
}

func (w *WithoutLicense) Run(c *gitlab.Client, p *gitlab.Project) bool {
return p.LicenseURL == ""
}

func (w *WithoutLicense) GetSlug() string {
return "without-license"
}

func (w *WithoutLicense) GetLevel() string {
return LevelWarning
}

func NewWithoutLicense() Ruler {
w := &WithoutLicense{
Name: "Without License",
Description: "Projects without a LICENSE file are not recommended.",
}
w.ID = w.GetSlug()
w.Level = w.GetLevel()
return w
}
```

The logic is straightforward - GitLab's API provides `LicenseURL` when a license exists, so we just check if it's empty.

## Adding Tests

Create `rules/without_license_test.go`:

```go
// Copyright (c) 2021, Marcelo Jorge Vieira
// Licensed under the BSD 3-Clause License

package rules

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/xanzy/go-gitlab"
)

func TestWithoutLicense(t *testing.T) {
tests := []struct {
name string
project *gitlab.Project
want bool
}{
{
name: "should return true if project does not have a license",
project: &gitlab.Project{
LicenseURL: "",
},
want: true,
},
{
name: "should return false if project has a license",
project: &gitlab.Project{
LicenseURL: "https://opensource.org/licenses/BSD-3-Clause",
},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
w := &WithoutLicense{}
got := w.Run(nil, tt.project)
assert.Equal(t, tt.want, got)
})
}
}
```

Run `make test` to verify everything works.

## Registering the Rule

Add your rule to `rules/my_registry.go`:

```go
func init() {
MyRegistry.AddRule(NewEmptyRepository())
MyRegistry.AddRule(NewGoVendorFolder())
// ... other rules
MyRegistry.AddRule(NewWithoutLicense())
}
```

## Testing It Out

Start the server with `make run-docker` and check the rules endpoint:

```bash
curl http://localhost:3000/api/v1/rules
```

You should see `without-license` in the response. That's it - your rule is now part of the system and will run on all projects during the next collection.

3 changes: 3 additions & 0 deletions rules/my_registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,6 @@ func init() {
MyRegistry.AddRule(NewWithoutReadme())
MyRegistry.AddRule(NewWithoutMakefile())
}

MyRegistry.AddRule(NewWithoutLicense())

36 changes: 36 additions & 0 deletions rules/without_license.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Copyright (c) 2021, Marcelo Jorge Vieira
// Licensed under the BSD 3-Clause License

package rules

import "github.com/xanzy/go-gitlab"

type WithoutLicense struct {
Description string `json:"description"`
ID string `json:"ruleId"`
Level string `json:"level"`
Name string `json:"name"`
}

func (w *WithoutLicense) Run(c *gitlab.Client, p *gitlab.Project) bool {
return p.LicenseURL == ""
}

func (w *WithoutLicense) GetSlug() string {
return "without-license"
}

func (w *WithoutLicense) GetLevel() string {
return LevelWarning
}

func NewWithoutLicense() Ruler {
w := &WithoutLicense{
Name: "Without License",
Description: "Projects without a LICENSE file are not recommended.",
}
w.ID = w.GetSlug()
w.Level = w.GetLevel()
return w
}

Loading