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
2 changes: 2 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@
.serena
.pruefbyte
*.exe
internal/ocrbin/bin
dist
44 changes: 37 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,21 +52,51 @@ jobs:
- run: go test -race -coverprofile=coverage.out ./...
- run: go tool cover -func=coverage.out | tail -1

# Builds every release binary (linux, macOS, windows; amd64, arm64), with ocr
# embedded, from the same config the release job uses, so a broken release
# shows up in the PR.
build:
runs-on: ubuntu-latest
strategy:
matrix:
goarch: [amd64, arm64]
steps:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version-file: go.mod
- run: go build -trimpath -o pruefbyte-linux-${{ matrix.goarch }} ./cmd/pruefbyte
- name: Cache OCR binaries embedded in release builds
uses: actions/cache@v6
with:
path: internal/ocrbin/bin
key: ocrbin-${{ hashFiles('internal/ocrbin/VERSION', 'internal/ocrbin/fetch/**') }}
- uses: goreleaser/goreleaser-action@v7
with:
version: v2.18.2
args: build --snapshot --clean

# Publishes the binaries for `pruefbyte local` as a GitHub release on v* tags.
release:
if: startsWith(github.ref, 'refs/tags/v')
needs: [lint, test, build]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0 # the changelog needs the previous tag
- uses: actions/setup-go@v7
with:
go-version-file: go.mod
- name: Cache OCR binaries embedded in release builds
uses: actions/cache@v6
with:
path: internal/ocrbin/bin
key: ocrbin-${{ hashFiles('internal/ocrbin/VERSION', 'internal/ocrbin/fetch/**') }}
- uses: goreleaser/goreleaser-action@v7
with:
version: v2.18.2
args: release --clean
env:
CGO_ENABLED: "0"
GOOS: linux
GOARCH: ${{ matrix.goarch }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

image:
needs: [lint, test, build]
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ coverage.*
/.pruefbyte/
/.ocr/

# OCR binaries downloaded for release builds (go run ./internal/ocrbin/fetch)
/internal/ocrbin/bin/

# Local configuration and secrets
.env
.env.*
Expand Down
50 changes: 50 additions & 0 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Release binaries for `pruefbyte local` on developer machines. CI runs this on
# v* tags; try it locally with: goreleaser release --snapshot --clean
version: 2

project_name: pruefbyte

before:
hooks:
# Downloads the OCR release pinned in internal/ocrbin/VERSION for every
# platform, checksum-verified, for embedding below.
- go run ./internal/ocrbin/fetch

builds:
- main: ./cmd/pruefbyte
binary: pruefbyte
env:
- CGO_ENABLED=0
flags:
- -trimpath
tags:
- embedocr # include ocr, so the download is all a developer needs
ldflags:
- -s -w -X main.version={{ .Version }}
goos: [linux, darwin, windows]
goarch: [amd64, arm64]

archives:
- formats: [tar.gz]
format_overrides:
- goos: windows
formats: [zip]
# No version in the name, so .../releases/latest/download/pruefbyte_linux_arm64.tar.gz
# always gets the newest release; the README's install commands rely on this.
name_template: "{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}"
files:
- LICENSE
- README.md
- src: internal/ocrbin/bin/LICENSE
dst: LICENSE.open-code-review

checksum:
name_template: checksums.txt

changelog:
use: github
sort: asc

release:
footer: |
Container image: `ghcr.io/feinarbyte/pruefbyte:{{ .Version }}` (linux/amd64, linux/arm64).
6 changes: 5 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,15 @@ RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH \
go build -trimpath -ldflags "-s -w -X main.version=${VERSION}" -o /out/pruefbyte ./cmd/pruefbyte

FROM --platform=$BUILDPLATFORM alpine:3 AS ocr
ARG OCR_VERSION=v1.12.10
# The OCR version is pinned in internal/ocrbin/VERSION, which the release
# binaries embed too, so the image and `pruefbyte local` run the same ocr.
# tr also drops the CR a Windows checkout (core.autocrlf) adds.
COPY internal/ocrbin/VERSION /tmp/OCR_VERSION
# BuildKit sets TARGETARCH for the platform being built; a default here would
# override it (e.g. an amd64 binary in an arm64 image).
ARG TARGETARCH
RUN apk add --no-cache curl \
&& OCR_VERSION="$(tr -d '[:space:]' < /tmp/OCR_VERSION)" \
&& arch="${TARGETARCH:-amd64}" \
&& cd /tmp \
&& curl -fsSLO "https://github.com/alibaba/open-code-review/releases/download/${OCR_VERSION}/opencodereview-linux-${arch}" \
Expand Down
133 changes: 115 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,22 @@ from a dedicated bot account.
## Setup

1. **Bot user.** Create a GitLab user (e.g. `pruefbyte-bot`) and add it to your group or projects as **Developer**. Create a personal access token for it with scope `api`.
2. **CI/CD variables** (group level, masked):
2. **CI/CD variables** (group level, masked). Only secrets go here:
| Variable | Value |
|---|---|
| `PRUEFBYTE_GITLAB_TOKEN` | the bot's PAT |
| `PRUEFBYTE_LLM_API_KEY` | the LLM API key |
| `PRUEFBYTE_LLM_PROVIDER` | e.g. `anthropic` |
| `PRUEFBYTE_LLM_MODEL` | e.g. `claude-sonnet-5` |

Provider, model and all review settings go into the repository's `.pruefbyte.yml`
(see [Configuration](#configuration)), so that `pruefbyte local` reviews with exactly
the same settings. A `PRUEFBYTE_LLM_MODEL` or similar CI variable would override the
file in CI only, and local runs would no longer match.
3. **Image.** CI publishes `ghcr.io/feinarbyte/pruefbyte` for linux/amd64 and linux/arm64:
`latest` from `main`, `X.Y.Z` and `X.Y` from `vX.Y.Z` tags, and `sha-<commit>` for each of these pushes.
If the package is private, give the GitLab runners pull access (a GitHub token with
`read:packages` in `DOCKER_AUTH_CONFIG`). To build your own:
```sh
docker buildx build --platform linux/amd64,linux/arm64 --build-arg OCR_VERSION=v1.12.10 \
docker buildx build --platform linux/amd64,linux/arm64 \
-t registry.example.com/tools/pruefbyte:latest --push .
```
4. **Pipeline.** Include the template in each project (or in a shared CI config):
Expand Down Expand Up @@ -57,13 +60,16 @@ Settings are layered; later layers win:
3. `.pruefbyte.yml` in the repository, **read from the merge request's base commit**. A merge request can't change its own review settings: config changes take effect once they are merged. The same holds for OCR's own rule files, `.opencodereview/rule.json` and `ocr.rule_file`: pruefbyte reads them at the base commit and passes one rule file that keeps the merge request's copies from applying.
4. Environment variables `PRUEFBYTE_<SECTION>_<KEY>`, e.g. `PRUEFBYTE_REVIEW_MIN_SEVERITY=medium`. Lists are comma-separated.

The repository file may only set `llm.model`, `ocr.*` (except `binary` and `extra_args`), and `review.*`. Anything that decides where credentials are sent, or what gets executed, is rejected there.
The repository file may set `llm.provider` (OCR built-in providers only, and only when the global config does not set one: a provider the operator names keeps the shared API key with that vendor), `llm.model`, `ocr.*` (except `binary` and `extra_args`) and `review.*`. Anything that decides where credentials are sent, or what gets executed, is rejected there: custom providers with their own `llm.url` belong in the global file.

Secrets are only ever read from the env vars named by `gitlab.token_env` and `llm.api_key_env`. `ocr` runs with a private, temporary `HOME`, so its config file and session logs never touch the runner.

Example `.pruefbyte.yml`:

```yaml
llm:
provider: anthropic
model: claude-sonnet-5
ocr:
effort: high
exclude: ["**/generated/**"]
Expand Down Expand Up @@ -116,15 +122,94 @@ Overriding `rules:` replaces the template's list, so keep its first two entries
| OCR fails | A failure note with the redacted error; job exits 1. |
| `review.fail_on_severity` reached | Comments are posted; job exits 3. |

## Local use
## Local review

Run the CI review on your machine before you push, so the bot has nothing left to say:

```sh
pruefbyte local
```

It reviews what your merge request will contain: everything from the merge base with
the target branch (default: origin's HEAD; or e.g. `--target origin/develop`) up to your
working tree, including staged, unstaged and untracked files. Your index, branch and
files stay untouched. `--committed` reviews only commits, which is exactly what CI sees
after a push. Findings print in the terminal (`--format json` for tools), and nothing
is posted.

The settings are the CI's, read the same way and from the same places: `.pruefbyte.yml`
and OCR rule files at the target branch, with the same rule merging, excludes, effort,
provider and model, and the same `review.min_severity` / `review.categories` filtering.
A finding that reaches `review.fail_on_severity` exits 3, as in CI, so the command
works as a pre-push hook. If you edit `.pruefbyte.yml` on your branch, the run tells
you that CI, and so the local run, uses the target branch's version until your change
is merged. One difference remains: CI gives OCR the merge request's title and
description as background; locally the branch name and commit messages stand in.

The API key is the first one found of:
1. the env var named by `llm.api_key_env` (`PRUEFBYTE_LLM_API_KEY`),
2. your own OCR setup (`~/.opencodereview/config.json`: `api_key` or `api_key_cmd`),
3. the provider's env var, e.g. `ANTHROPIC_API_KEY`.

No GitLab token is needed. If your CI uses a global config file (`PRUEFBYTE_CONFIG`),
pass the same file with `--config`.

### Install

Release binaries include OpenCodeReview (`ocr`), the exact version CI uses, so a
single download is all you need. On first use pruefbyte unpacks it into your user cache
directory. Pick whichever install suits you:

**With mise:**

```sh
mise use -g github:feinarbyte/pruefbyte
```

**Linux / macOS**, latest release into `~/.local/bin`:

```sh
export PRUEFBYTE_GITLAB_TOKEN=glpat-... PRUEFBYTE_LLM_API_KEY=sk-...
pruefbyte review --config pruefbyte.yml --gitlab-url https://gitlab.example.com \
--project group/project --mr 42 --repo . --dry-run
os=$(uname -s | tr '[:upper:]' '[:lower:]'); arch=$(uname -m | sed 's/x86_64/amd64/; s/aarch64/arm64/')
mkdir -p ~/.local/bin
curl -fsSL "https://github.com/feinarbyte/pruefbyte/releases/latest/download/pruefbyte_${os}_${arch}.tar.gz" \
| tar -xz -C ~/.local/bin pruefbyte
```

`--dry-run` prints the discussions instead of posting them. `pruefbyte config print` shows the effective configuration.
**Windows** (PowerShell), latest release into `%LOCALAPPDATA%\Programs\pruefbyte`:

```powershell
$arch = if ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64') { 'arm64' } else { 'amd64' }
$dir = "$env:LOCALAPPDATA\Programs\pruefbyte"; $zip = "$env:TEMP\pruefbyte.zip"
Invoke-WebRequest "https://github.com/feinarbyte/pruefbyte/releases/latest/download/pruefbyte_windows_$arch.zip" -OutFile $zip
Expand-Archive $zip $dir -Force; Remove-Item $zip
[Environment]::SetEnvironmentVariable('Path', "$([Environment]::GetEnvironmentVariable('Path', 'User'));$dir", 'User')
```

Each release also lists the archives with a `checksums.txt`
([releases](https://github.com/feinarbyte/pruefbyte/releases)).

**With Go** 1.25 or newer. This builds from source without OCR, so `ocr` must be on
your PATH as well:

```sh
go install github.com/feinarbyte/pruefbyte/cmd/pruefbyte@latest
npm install -g @alibaba-group/open-code-review # or: brew install open-code-review
```

**With Docker**, with `ocr` included and nothing to install. Run it as yourself so
new git objects in your repository stay yours:

```sh
docker run --rm -it --user "$(id -u):$(id -g)" -v "$PWD:/repo" -w /repo \
-e PRUEFBYTE_LLM_API_KEY ghcr.io/feinarbyte/pruefbyte pruefbyte local
```

Check the install with `pruefbyte version`; it also says whether `ocr` is built in.
An `ocr.binary` setting overrides the built-in copy.

To try the CI path against a real merge request without posting, set
`PRUEFBYTE_GITLAB_TOKEN` and run `pruefbyte review --project group/project --mr 42 --dry-run`.
`pruefbyte config print` shows the effective configuration.

## Development

Expand All @@ -135,11 +220,21 @@ golangci-lint run ./...
go test -race ./...
```

GitHub Actions (`.github/workflows/ci.yml`) runs these checks plus `go mod tidy` and
linux/amd64 + linux/arm64 builds on pushes to `main` and `v*` tags and on every pull
request. Once they pass, it
builds the multi-arch image. On `main` and `v*` tags the image is pushed to GHCR;
for pull requests it is only built.
GitHub Actions (`.github/workflows/ci.yml`) runs these checks plus `go mod tidy` on
pushes to `main` and `v*` tags and on every pull request. It also builds all release
binaries (Linux, macOS and Windows; amd64 and arm64) with GoReleaser
(`.goreleaser.yaml`). Once these pass, it builds the multi-arch image. On `main` and
`v*` tags the image is pushed to GHCR; for pull requests it is only built.

To release, push a tag such as `v0.1.0`. CI then publishes the image tags `0.1.0`
and `0.1`, plus a GitHub release with the binaries and checksums. Try the release
build locally with `goreleaser release --snapshot --clean`.

The OCR version is pinned in `internal/ocrbin/VERSION`, for both the Docker image and
the release binaries. To update OCR, change that file. Release builds use the
`embedocr` build tag and embed the gzip-compressed `ocr` that
`go run ./internal/ocrbin/fetch` downloads and checks against OCR's published
checksums. Plain `go build` and `go test` need neither.

Layout:

Expand All @@ -148,6 +243,7 @@ Layout:
| `cmd/pruefbyte` | CLI (cobra) and wiring |
| `internal/config` | layered config, repo-file allow-list, env overrides |
| `internal/ocr` | drives the `ocr` CLI and parses its JSON |
| `internal/ocrbin` | the pinned OCR version; the `ocr` embedded in release builds (`fetch` downloads it) |
| `internal/gitlab` | client-go wrapper bound to one MR; dry-run decorator |
| `internal/review` | orchestration: filter, place, dedupe, publish, resolve |
| `internal/gitutil` | reads the repo config at the base commit; fetches missing commits |
Expand All @@ -158,6 +254,7 @@ OCR is used as a subprocess. Its Go packages all live under `internal/`, so they

pruefbyte is released under the [MIT License](LICENSE).

The Docker image also bundles the OpenCodeReview (`ocr`) binary, which is
licensed under the [Apache License 2.0](https://github.com/alibaba/open-code-review/blob/main/LICENSE).
Both license texts are in the image under `/usr/share/licenses/`.
The Docker image and the release binaries also bundle the OpenCodeReview (`ocr`)
binary, which is licensed under the [Apache License 2.0](https://github.com/alibaba/open-code-review/blob/main/LICENSE).
Both license texts are in the image under `/usr/share/licenses/`, and in each release
archive as `LICENSE` and `LICENSE.open-code-review`.
Loading
Loading