gocov
Self-hostable coverage tracking — an open-source Coveralls/Codecov
alternative. Single binary + Postgres. Bitbucket Cloud is the first
supported forge. Supported formats: Go cover profiles, LCOV tracefiles
(JavaScript/TypeScript — Jest, Vitest, nyc, c8), JaCoCo XML
(Java/Kotlin — Maven, Gradle, Android) and Cobertura XML
(Python — coverage.py/pytest-cov; also PHPUnit, coverlet, gcovr); the
format is detected from the uploaded content.
Features (MVP)
- Parses Go cover profiles (
go test -coverprofile), LCOV tracefiles
(Jest/Vitest/nyc lcov.info), JaCoCo XML (jacoco.xml) and Cobertura
XML (coverage.xml) into total and per-file coverage
POST /api/v1/upload API with per-repo Bearer tokens
- SVG coverage badge per repo (
/badge/{workspace}/{repo}.svg)
- Web UI: repo list → upload list → per-file coverage table
- Uploader CLI that auto-detects Bitbucket Pipelines environment
variables and falls back to git
- Pushes a
coverage: X% (±Y%) build status to Bitbucket commits when
the repo has an app password configured
- Coverage gate: per-repo minimums for total and diff coverage plus a
drop tolerance; violations push a FAILED build status, so a Bitbucket
merge check can block the PR
- Source view: any file in an upload renders line by line with coverage
overlay and hit counts, fetched from the forge at the exact commit and
cached immutably (misses are cached too); without forge credentials
the page falls back to an uncovered-line summary. When an upload has
no
path_prefix, recorded paths that carry an unmapped leading
prefix (a Go module path, a CI checkout directory) are resolved by
probing trimmed variants against the forge
- Web UI sign-in with Bitbucket: configure an OAuth consumer and every
page requires login, allowed only for members of the workspaces the
instance tracks (see "Enable Bitbucket sign-in"). Uploads, badges and
health checks are unaffected; no passwords are ever stored
- Diff coverage for pull requests: fetches the PR diff from Bitbucket,
intersects changed lines with coverage blocks, and posts a PR comment
listing uncovered changed lines — repeated uploads update the same
comment instead of stacking new ones
The architecture is deliberately extensible: coverage formats sit behind
profile.Parser, forges behind forge.Forge, raw profile storage behind
blobstore.Store, and the database schema stores a format-agnostic
normalized model — so lcov/cobertura, GitHub/GitLab, diff coverage, and
S3 storage can be added without rewrites.
Quick start
docker compose up
This starts Postgres and the server on http://localhost:8080 (migrations
apply automatically).
Onboarding a whole workspace (recommended)
For many repos, register the workspace once and use a single token:
docker compose exec server gocov-server workspace add \
-prefix myworkspace -default-branch main
Set the printed token as a Bitbucket workspace variable (GOCOV_TOKEN,
secured) together with GOCOV_SERVER — every repo inherits them. Repos
register themselves on their first upload; their default branch is asked
from Bitbucket when a global bot account is configured (see
Configuration), falling back to the workspace's -default-branch.
Registering repos one by one
docker compose exec server gocov-server repo add \
-slug myworkspace/myrepo \
-default-branch main \
-bb-username myuser -bb-app-password "$APP_PASSWORD" # optional, for build statuses
Manage repos later with:
gocov-server repo list # slugs, branches, credential status
gocov-server repo rotate-token -slug myworkspace/myrepo # invalidates the old token
gocov-server repo update -slug myworkspace/myrepo \
-default-branch develop # and/or -bb-username/-bb-app-password,
# or -clear-credentials
gocov-server repo remove -slug myworkspace/myrepo -force # deletes uploads and raw profiles too;
# without -force only prints a summary
gocov-server workspace list|rotate-token|update|remove # workspace token management
Enable Bitbucket sign-in
Out of the box the web UI is open and shows a banner saying so — nothing
changes on upgrade until you opt in. To require sign-in:
- In Bitbucket, create an OAuth consumer under Workspace settings →
OAuth consumers → Add consumer with
- Callback URL:
https://your-gocov-host/oauth/bitbucket/callback
(must be exactly GOCOV_BASE_URL + /oauth/bitbucket/callback)
- Permissions: Account: Read and Email only — nothing broader
is needed
- Set the consumer's key and secret on the server:
GOCOV_OAUTH_BITBUCKET_KEY=...
GOCOV_OAUTH_BITBUCKET_SECRET=...
From then on every UI page requires signing in with a Bitbucket account.
Access is decided at login time by workspace membership: by default,
members of any workspace the instance tracks (registered workspaces and
the workspace part of registered repo slugs) may sign in, and everyone
else gets a clear denial page. Set GOCOV_ALLOWED_WORKSPACES (comma-
separated workspace slugs) to replace the derived set with an explicit
list. Accounts are provisioned on first successful sign-in — there is
no user bookkeeping, and gocov never sees or stores passwords (the
Bitbucket tokens are discarded right after login).
CI is unaffected either way: the upload API keeps its Bearer tokens,
badges stay embeddable, /healthz stays open.
gocov-server user list # who has signed in
gocov-server user remove -email jane@example.com # revoke immediately
Removal deletes the account and its sessions; the person can sign in
again (and is re-provisioned) as long as they are still a workspace
member. Sessions last 30 days; membership is re-checked at each login,
not per request.
Coverage gate
gocov-server repo update -slug myworkspace/myrepo \
-min-coverage 80 -min-diff-coverage 70 -max-drop 0.5
Each rule is optional: -min-coverage is the minimum total percentage,
-min-diff-coverage applies to the changed lines of PR uploads (skipped
when no diff coverage is available), and -max-drop bounds how far
total coverage may fall below the latest gate-passing upload on the
default branch (0 forbids any drop). Gate-failing uploads are recorded
but never serve as a baseline, so re-running CI cannot launder a failure
and a PR cannot ratchet coverage down push by push. Violations mark the
pushed build status FAILED — require the gocov build in Bitbucket's
merge checks to block such PRs — and are reported in the PR comment and
the upload response (gate field). -clear-gate removes all rules. The
same flags on workspace add and workspace update set defaults
inherited by auto-registered repos. The CLI exits non-zero on a failed
gate when run with -fail-on-gate.
Uploading coverage from CI
In Bitbucket Pipelines (commit, branch, repo and PR id are auto-detected):
- step:
script:
- go test ./... -covermode=atomic -coverprofile=coverage.out
- go run github.com/bykclk/gocov/cmd/gocov@latest upload coverage.out
with GOCOV_SERVER and GOCOV_TOKEN set as repository variables.
On runners without a Go toolchain, use the prebuilt binaries from
GitHub Releases instead
(linux/darwin/windows, amd64 + arm64, checksums included). Pin a version
and cache the download on self-hosted runners:
ver=v0.1.0
arch=$(uname -m); case "$arch" in x86_64) arch=amd64;; aarch64|arm64) arch=arm64;; esac
bin="$HOME/.cache/gocov/gocov-$ver-linux-$arch"
if [ ! -x "$bin" ]; then
mkdir -p "$(dirname "$bin")"
curl -fsSL "https://github.com/bykclk/gocov/releases/download/$ver/gocov-linux-$arch" -o "$bin"
chmod +x "$bin"
fi
"$bin" upload coverage.out
Outside CI, values fall back to git or can be passed explicitly:
gocov upload -server https://gocov.example -token $TOKEN \
-repo myworkspace/myrepo -commit $(git rev-parse HEAD) -branch main \
coverage.out
Other ecosystems upload their reports the same way — the format is
detected from the content, no flag needed:
npx jest --coverage # or vitest run --coverage, nyc, c8 ...
gocov upload coverage/lcov.info
mvn verify # with the jacoco-maven-plugin
gocov upload target/site/jacoco/jacoco.xml
gradle test jacocoTestReport # xml.required = true
gocov upload build/reports/jacoco/test/jacocoTestReport.xml
pytest --cov --cov-report=xml # coverage.py / pytest-cov
gocov upload coverage.xml
JaCoCo paths are package-qualified (com/example/Foo.java); diff
coverage matches them against repo paths by suffix, so source roots like
src/main/java need no configuration.
Badge

Red below 50%, yellow 50–75%, green above 75%. Shows the latest upload on
the repo's default branch.
API
POST /api/v1/upload — multipart form, Authorization: Bearer <token>
| part |
meaning |
profile |
file: the coverage profile |
repo |
optional; must match the token's repo |
commit |
required commit SHA |
branch |
defaults to the repo's default branch |
pr_id |
optional pull request id |
format |
go, lcov, jacoco or cobertura; omitted → detected from content |
path_prefix |
maps profile paths to repo paths for diff coverage, e.g. the Go module path (the CLI fills it from go.mod) |
Returns 201 with {id, total_pct, covered_stmts, total_stmts, delta_pct, build_status}. Uploads carrying a pr_id additionally get
diff_pct, diff_covered_lines, diff_total_lines, diff_status and
pr_comment when the repo has forge credentials configured.
Configuration
| variable |
default |
|
DATABASE_URL |
— |
Postgres DSN (required) |
GOCOV_ADDR |
:8080 |
listen address |
GOCOV_BASE_URL |
http://localhost:8080 |
public URL used in statuses |
GOCOV_BITBUCKET_USERNAME |
— |
global Bitbucket bot account (with an API token, the account email) |
GOCOV_BITBUCKET_APP_PASSWORD |
— |
the bot's app password or scoped API token |
GOCOV_OAUTH_BITBUCKET_KEY |
— |
OAuth consumer key; with the secret, turns on web UI sign-in |
GOCOV_OAUTH_BITBUCKET_SECRET |
— |
OAuth consumer secret |
GOCOV_ALLOWED_WORKSPACES |
derived from tracked repos |
comma-separated workspace slugs allowed to sign in |
The global bot account is used by every repo that has no credentials of
its own — for build statuses, PR comments, diff coverage and default
branch detection. Per-repo credentials (repo update -bb-username ...)
take precedence.
Development
go test ./...
go build ./...
The store, forge and blobstore interfaces each have test doubles
(internal/store/memory, internal/forge/fake,
internal/blobstore/memory), so handlers are fully testable without
Postgres or Bitbucket.
The Postgres store additionally has integration tests that run against a
real server when GOCOV_TEST_DATABASE_URL is set (they are skipped
otherwise). Each test creates and drops its own scratch database:
docker run --rm -d --name gocov-test-db -p 5433:5432 \
-e POSTGRES_USER=gocov -e POSTGRES_PASSWORD=gocov -e POSTGRES_DB=gocov \
postgres:16-alpine
GOCOV_TEST_DATABASE_URL=postgres://gocov:gocov@localhost:5433/gocov go test ./...
docker stop gocov-test-db
GET /healthz reports readiness (checks database connectivity) for load
balancers and container orchestrators; the server shuts down gracefully
on SIGINT/SIGTERM.
License
AGPL-3.0 — see LICENSE.