name: Tests
on:
push:
branches: [main]
pull_request:
env:
# The canonical fossil the real-fossil-gated tests compare against. Local
# development validates against this exact version, so CI must too --
# otherwise the two are checking our output against different reference
# implementations. Bump deliberately, here, in one place.
#
# The distro package is unusable for this: it is unpinned, currently lags
# several releases behind, and tracks different upstream check-ins over time
# under the same version string, so two runs at nominally the same version
# can behave differently. The official upstream release tarball is a fixed
# build, and FOSSIL_SHA256 pins it to that exact artifact -- a re-upload
# upstream then fails the checksum loudly instead of silently changing what
# we test against.
FOSSIL_VERSION: "2.28"
FOSSIL_SHA256: "c9d2bf9d150dff1076eae1b0b822ed3cde892e1273f78b040c4f8ace1b59dd40"
jobs:
test:
name: Test (modernc)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Install pinned Fossil
run: |
curl -fsSL --retry 3 --retry-delay 2 --retry-all-errors \
-o /tmp/fossil.tar.gz \
"https://fossil-scm.org/home/uv/fossil-linux-x64-${FOSSIL_VERSION}.tar.gz"
echo "${FOSSIL_SHA256} /tmp/fossil.tar.gz" | sha256sum -c -
sudo tar -xzf /tmp/fossil.tar.gz -C /usr/local/bin fossil
rm -f /tmp/fossil.tar.gz
- name: Verify Fossil version
# A silent upstream change has to surface as an explicit failure here,
# not as mysterious behavioural drift in the real-fossil-gated tests.
run: |
reported=$(fossil version | sed -n 's/.*fossil version \([0-9][0-9.]*\).*/\1/p')
if [ "$reported" != "${FOSSIL_VERSION}" ]; then
echo "expected fossil ${FOSSIL_VERSION}, got: $(fossil version)" >&2
exit 1
fi
fossil version
- name: Warm Fossil global config
# Fossil keeps a per-user global config database outside any repository
# and creates it lazily on first use. `go test` runs packages
# concurrently, several of them shell out to fossil, and nothing sets
# HOME, so on a fresh runner two test processes can race to create it:
# one wins and the others die with "table global_config already exists".
# Creating it once here, single-threaded, means no test process is ever
# the one to create it. `fossil version` does not open that database, so
# the step above does not incidentally cover this; `fossil all list`
# does, and prints nothing.
#
# The first call's exit status is ignored deliberately -- it exists to
# create the database, not to assert anything, so an unrelated non-zero
# must not red the job. The second call is what carries the weight: it
# must succeed on its own, which proves the database is both present and
# usable. Checking that a file exists would not -- a truncated or
# partially-written database passes an existence check while concurrent
# invocations still die on the missing schema, which is exactly the
# "looks like protection while the race stays live" failure this guards
# against. Re-running the command also keeps the check independent of
# where fossil resolved the database to (~/.fossil, ~/.config/fossil.db,
# $XDG_CONFIG_HOME, or $FOSSIL_HOME).
run: |
fossil all list >/dev/null 2>&1 || true
fossil all list >/dev/null
- name: Build all packages
# Build every package, not just the cmd/libfossil dependency closure.
# The `go test $(go list ./...)` step below silently drops any package
# that fails to compile (go list omits it from stdout instead of
# failing), so a broken import can vanish from the test run undetected.
# Building `./...` first makes that a loud, fail-fast build error.
run: GOWORK=off go build ./...
- name: Unit tests
# REQUIRE_FOSSIL_BIN turns "fossil is missing" into a failure rather
# than an invisible skip. Verifying a real fossil reads what we write
# is the one thing our own tests cannot substitute for.
env:
REQUIRE_FOSSIL_BIN: "1"
run: GOWORK=off go test $(GOWORK=off go list ./... | grep -v '/dst') -count=1 -timeout=120s
- name: DST
run: GOWORK=off go test ./dst/... -count=1 -timeout=300s
- name: Driver tests (modernc)
run: cd db/driver/modernc && GOWORK=off go test ./... -count=1
- name: Vet
run: GOWORK=off go vet ./...
test-ncruces:
name: Test (ncruces)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Install pinned Fossil
run: |
curl -fsSL --retry 3 --retry-delay 2 --retry-all-errors \
-o /tmp/fossil.tar.gz \
"https://fossil-scm.org/home/uv/fossil-linux-x64-${FOSSIL_VERSION}.tar.gz"
echo "${FOSSIL_SHA256} /tmp/fossil.tar.gz" | sha256sum -c -
sudo tar -xzf /tmp/fossil.tar.gz -C /usr/local/bin fossil
rm -f /tmp/fossil.tar.gz
- name: Verify Fossil version
# A silent upstream change has to surface as an explicit failure here,
# not as mysterious behavioural drift in the real-fossil-gated tests.
run: |
reported=$(fossil version | sed -n 's/.*fossil version \([0-9][0-9.]*\).*/\1/p')
if [ "$reported" != "${FOSSIL_VERSION}" ]; then
echo "expected fossil ${FOSSIL_VERSION}, got: $(fossil version)" >&2
exit 1
fi
fossil version
- name: Warm Fossil global config
# Same single-threaded warm-up as the modernc leg -- see the comment
# there for why `fossil version` does not cover this, and why the first
# call's exit status is ignored but the second call's is not.
run: |
fossil all list >/dev/null 2>&1 || true
fossil all list >/dev/null
- name: Build all packages (ncruces)
# Same fail-fast guard as the modernc leg, under the test_ncruces tag so
# the ncruces driver's compile surface is covered too.
run: GOWORK=off go build -tags test_ncruces ./...
- name: Unit tests (ncruces)
env:
REQUIRE_FOSSIL_BIN: "1"
run: GOWORK=off go test -tags test_ncruces $(GOWORK=off go list ./... | grep -v '/dst') -count=1 -timeout=120s
- name: DST (ncruces)
run: GOWORK=off go test -tags test_ncruces ./dst/... -count=1 -timeout=300s
- name: Driver tests (ncruces)
run: cd db/driver/ncruces && GOWORK=off go test ./... -count=1
build-wasm:
name: Build (js/wasm)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Build main module for js/wasm
# Browser support is the reason the ncruces driver is in this repo at
# all, but until this job nothing was ever compiled for js/wasm.
#
# What a build gate catches is a dependency with no js/wasm build at
# all -- most realistically the wrong SQLite driver reaching a shared
# package, since modernc.org/libc is linux/darwin-only and would fail
# here while both linux legs above stay green. It does not catch every
# WASM problem: much of the stdlib (os/exec included) compiles for js
# behind stubs and only fails when called, so runtime-behaviour gaps
# need tests under a browser harness, which is a separate question.
#
# test_ncruces is the same tag the ncruces leg builds under. It switches
# both internal/testdriver and cmd/libfossil from modernc to ncruces,
# which is the driver that can actually target WASM. Without the tag,
# both packages would pull modernc into this build.
#
# The package set comes from `go list`, not a hand-written list, so a
# newly added package is inside the gate by default rather than
# silently outside it. `-e` stops go list from aborting on the package
# excluded below -- without it that unresolvable import would
# discard the entire list. Packages that already declare `//go:build
# !js` in source (internal/stash, internal/undo) are dropped by go list
# itself under GOOS=js; that exclusion lives in the code, not here.
#
# The only exclusion is examples/embed-repo-api: its main package
# imports the modernc driver directly, and modernc.org/libc ships no
# js/wasm build. cmd/libfossil is deliberately built here with the
# test_ncruces binding, so this gate compiles the tagged CLI for WASM.
# examples/embed-repo-api stays fully covered by the linux legs above.
env:
GOWORK: "off"
GOOS: js
GOARCH: wasm
run: |
set -o pipefail
pkgs=$(go list -e -tags test_ncruces ./... \
| grep -v -e '/examples/embed-repo-api$')
echo "building $(echo "$pkgs" | wc -l) packages for js/wasm:"
echo "$pkgs"
echo "$pkgs" | xargs go build -tags test_ncruces
- name: Build ncruces driver for js/wasm
# A separate module, so the step above never reaches it -- and it is
# the driver the browser build actually runs on. Its js-only source
# (ncruces_js.go) is compiled by no other job in this workflow.
env:
GOWORK: "off"
GOOS: js
GOARCH: wasm
run: cd db/driver/ncruces && go build ./...
test-otel:
name: Test (observer/otel)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: OTel sub-module tests
run: cd observer/otel && GOWORK=off go test ./... -count=1
test-docs:
name: Test (docs/site)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Install Hugo (extended)
# docs/site pulls the hextra theme via Hugo Modules, which need both the
# Go toolchain (setup-go above) and the Hugo binary. Pin the version so a
# future Hugo release can't silently change build behaviour under us.
env:
HUGO_VERSION: "0.162.1"
run: |
curl -fsSL "https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-amd64.tar.gz" \
| sudo tar -xz -C /usr/local/bin hugo
- name: Build docs site
# docs/site is the one module with no Go source of its own: its go.mod
# exists only to import the hextra theme as a Hugo Module, so `go build
# ./...` there matches no packages and gates nothing. A real Hugo build
# is the equivalent compile gate — it resolves the module import,
# compiles templates, and renders content, failing loud on a broken
# import or template the way the Go build steps do for the other modules.
run: cd docs/site && GOWORK=off hugo --gc --minify