Skip to content

release v0.12.0: pin intra-repo gosqlite.org requires to v0.12.0 #39

release v0.12.0: pin intra-repo gosqlite.org requires to v0.12.0

release v0.12.0: pin intra-repo gosqlite.org requires to v0.12.0 #39

Workflow file for this run

name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
# Run the full test suite on the three platforms most consumers actually
# use. Cross-build coverage for the long tail of modernc-supported targets
# is in build_all_targets below.
test:
name: test
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
# AGENTS.md / docs promise the two most recent Go releases;
# keep both here so a regression on the older line surfaces.
go: ['1.25.x', '1.26.x']
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v5
- uses: ./.github/actions/setup
with:
go-version: ${{ matrix.go }}
- name: go test ./...
run: go test -count=1 -timeout 5m ./...
# Race-mode test pass — linux/amd64 only because race-detection support is
# spotty on other targets and the bulk of the locking surface lives in
# platform-independent Go code anyway.
race:
name: race
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: ./.github/actions/setup
- name: go test -race
run: go test -race -count=1 -timeout 10m ./...
# Static analysis: go vet (always available) plus staticcheck (matches the
# modernc/sqlite CI lint pass).
lint:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: ./.github/actions/setup
- name: go vet
# unsafeptr=false silences the uintptr↔unsafe.Pointer false positives
# in the modernc-derived wrapper (see .golangci.yml + justfile vet
# recipe for the same suppression).
run: go vet -unsafeptr=false ./...
- name: gofmt -d
run: |
out=$(gofmt -d $(find . -name '*.go' -not -path './.*/*'))
if [ -n "$out" ]; then echo "$out"; exit 1; fi
- name: staticcheck
# We invoke staticcheck directly rather than dominikh/staticcheck-action
# because the action's latest tag (v1) still runs on Node.js 20, which
# GitHub is deprecating. Upstream has not cut a Node.js 24 release;
# running the binary on the existing toolchain bypasses the deprecated
# action entirely and produces identical lint output.
run: |
go install honnef.co/go/tools/cmd/staticcheck@latest
staticcheck ./...
- name: golangci-lint
# v8 of the action defaults to golangci-lint v2.x, which is the
# major version `.golangci.yml` is written against (`version: "2"`).
# We also pin the binary to a v2 release explicitly so a future
# action revision that flips the default cannot silently downgrade
# us back into the v1 series. v2.x is built with Go 1.25, which
# matches go.mod and avoids the "Go language version used to build
# golangci-lint is lower than the targeted Go version" error that
# v1.x throws on Go 1.25 modules.
uses: golangci/golangci-lint-action@v9
with:
version: v2.4.0
args: --timeout 5m
- name: gopls modernize
# Surfaces Go-version-bump idioms (range-over-int, reflect.TypeFor,
# strings.SplitSeq, sync.WaitGroup.Go, etc.). Skips the forked
# modernc/glebarez files we keep verbatim per CLAUDE.md — those
# follow upstream patterns and shouldn't drift on style.
#
# `^go:` strips Go's auto-toolchain breadcrumbs (`go: downloading
# ...`, `go: switching to go1.X`) — newer gopls releases require
# a newer Go than we pin in go.mod, so `go run @latest` will
# auto-download and that chatter would otherwise trip the
# `[ -n "$out" ]` gate.
run: |
out=$(go run golang.org/x/tools/gopls/internal/analysis/modernize/cmd/modernize@latest ./... 2>&1 \
| grep -v -E '^/[^:]*/(sqlite|vtab|rows)\.go:' \
| grep -v -E '^/[^:]*/gorm/(sqlite|migrator)\.go:' \
| grep -v '^exit status' \
| grep -v '^go: ' \
|| true)
if [ -n "$out" ]; then echo "$out"; exit 1; fi
# Cross-build every modernc-supported GOOS/GOARCH so we catch
# platform-specific compile errors without running the (often unavailable)
# test runtime there. The matrix mirrors modernc/sqlite's builder.json.
build_all_targets:
name: build (${{ matrix.goos }}/${{ matrix.goarch }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- { goos: darwin, goarch: amd64 }
- { goos: darwin, goarch: arm64 }
- { goos: freebsd, goarch: amd64 }
- { goos: freebsd, goarch: arm64 }
- { goos: linux, goarch: 386 }
- { goos: linux, goarch: amd64 }
- { goos: linux, goarch: arm }
- { goos: linux, goarch: arm64 }
- { goos: linux, goarch: loong64 }
- { goos: linux, goarch: ppc64le }
- { goos: linux, goarch: riscv64 }
- { goos: linux, goarch: s390x }
- { goos: windows, goarch: 386 }
- { goos: windows, goarch: amd64 }
- { goos: windows, goarch: arm64 }
steps:
- uses: actions/checkout@v5
- uses: ./.github/actions/setup
- name: cross build
env:
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
# modernc.org/sqlite/vec is only transpiled for the same OS set we
# build the package on (linux/darwin/freebsd/openbsd/windows).
# `go build ./vec/...` on excluded targets fails because the
# underlying lib hasn't been generated there; the `|| true` is
# deliberate so the cross-build matrix doesn't fail on platforms
# we never claimed to support for vec.
CGO_ENABLED: '0'
run: |
go build ./
go build ./fts/...
go build ./vfs/...
go build ./vec/... || echo "vec not supported on ${GOOS}/${GOARCH} — skipping"
# Cross-build every PUBLISHED sub-module (own go.mod, module path
# gosqlite.org/<sub> — gorm, blobstore, vfs/crypto). Discovered, not
# hand-listed; mirrors `just cross-build`'s pubmods loop. Build the
# package (`./`), not `./...`: some module examples import
# gosqlite.org/vec, which isn't transpiled on every target.
for f in $(find . -mindepth 2 -name go.mod -not -path './.*/*' -not -path './examples/*'); do
grep -q '^module gosqlite[.]org/' "$f" || continue
(cd "$(dirname "$f")" && go build ./)
done
# Run the SQL conformance suite under tests/sql/. This is our methodical
# feature-by-feature proof that every documented SQLite SQL surface the
# driver exposes actually works. See dev/coverage/sql.md for the matrix.
sql_conformance:
name: sql-conformance
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v5
- uses: ./.github/actions/setup
- name: go test ./tests/sql/...
run: go test -count=1 -timeout 2m -v ./tests/sql/...
# Run the vendored subset of modernc.org/sqlite's own test suite against
# this fork. See dev/upstream/modernc.md for what's in/out and why.
# The `modernc_upstream` build tag gates this so the default `go test`
# path stays fast.
modernc_upstream:
name: modernc-upstream
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: ./.github/actions/setup
- name: go test -tags=modernc_upstream
run: go test -tags=modernc_upstream -count=1 -timeout 5m -v .
# Run the vendored subset of mattn/go-sqlite3's test suite against this
# fork. See dev/upstream/mattn.md for what's in/out and why — the
# subset is small because mattn's tests probe mattn-internal types
# heavily, not the database/sql contract. This lane is a canary for
# the mattn-compat surface (math UDFs, unlock-notify semantics, DSN
# flag handling), not a parity proof.
mattn_upstream:
name: mattn-upstream
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: ./.github/actions/setup
- name: go test -tags=mattn_upstream
run: go test -tags=mattn_upstream -count=1 -timeout 5m -v -run 'TestMath|TestUnlock' .
# Every sub-module has its OWN go.mod (joined to the root via `replace
# gosqlite.org`), so its private deps never reach the root graph: blobstore's
# codec, vfs/crypto's adiantum + x/crypto, xorm-compat's xorm.io/xorm, gorm's
# dialector deps. Each is linted (vet + staticcheck + golangci-lint + modernize)
# and tested in its own module context. (xorm-compat is the CI-enforced proof
# gosqlite.org is a drop-in xorm SQLite driver — see dev/upstream/xorm.md.)
#
# The matrix is DISCOVERED, not hand-listed: `discover` finds every joined
# sub-module's go.mod and emits the dir list as JSON; `submodules` fans out
# over it. Adding or removing a module needs no edit here — it mirrors
# `just test-submodules`. gorm's own unit tests run here, so the gorm-upstream
# job below stays focused on the upstream integration suite.
discover:
name: discover submodules
runs-on: ubuntu-latest
outputs:
mods: ${{ steps.find.outputs.mods }}
steps:
- uses: actions/checkout@v5
- id: find
# Joined sub-modules = own go.mod + a `replace gosqlite.org` directive,
# minus the hidden reference clones (.xorm, …) and the examples/* modules.
run: |
mods=$(for f in $(find . -mindepth 2 -name go.mod -not -path './.*/*' -not -path './examples/*'); do
grep -q 'replace gosqlite.org ' "$f" && dirname "$f" | sed 's|^[.]/||'
done | sort | jq -R . | jq -sc .)
echo "mods=$mods" >> "$GITHUB_OUTPUT"
echo "discovered submodules: $mods"
submodules:
name: submodule (${{ matrix.dir }})
needs: discover
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
dir: ${{ fromJson(needs.discover.outputs.mods) }}
steps:
- uses: actions/checkout@v5
- uses: ./.github/actions/setup
# Same lint gate as the root `lint` job, in the sub-module's own context.
# gofmt is not repeated here — the root job's `gofmt -d` already covers
# every .go file in the repo, sub-modules included.
- name: go vet
working-directory: ${{ matrix.dir }}
run: go vet -unsafeptr=false ./...
- name: staticcheck
working-directory: ${{ matrix.dir }}
run: |
go install honnef.co/go/tools/cmd/staticcheck@latest
staticcheck ./...
- name: golangci-lint
uses: golangci/golangci-lint-action@v9
with:
version: v2.4.0
# Runs in the sub-module dir; pin the repo-root config explicitly so
# resolution can't drift (rather than relying on the walk-up), and so
# its exclusions apply uniformly across modules.
args: --timeout 5m --config ${{ github.workspace }}/.golangci.yml
working-directory: ${{ matrix.dir }}
- name: gopls modernize
working-directory: ${{ matrix.dir }}
# Forked upstream files (gorm's sqlite.go/migrator.go) are excluded by
# path tail, since modernize runs with the sub-module as the working dir.
run: |
out=$(go run golang.org/x/tools/gopls/internal/analysis/modernize/cmd/modernize@latest ./... 2>&1 \
| grep -v -E '(^|/)(sqlite|vtab|rows|migrator)\.go:' \
| grep -v '^exit status' | grep -v '^go: ' || true)
if [ -n "$out" ]; then echo "$out"; exit 1; fi
- name: go test (${{ matrix.dir }} module)
working-directory: ${{ matrix.dir }}
run: go test -count=1 -timeout 5m -v ./...
# Standing guard that every PUBLISHED sub-module pins the same gosqlite.org
# core version. A lagging pin is exactly what once made `go get
# gosqlite.org/gorm` fail with an ambiguous import (the submodule required an
# old core that still bundled gorm/). Mirrors `just check-pins`; `just release`
# keeps them in lockstep by construction.
module_pins:
name: module-pins
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: core-version pins are in lockstep
# Mirrors `just check-pins`; identical associative-array-free logic so
# the guard behaves the same on any bash.
run: |
vers=""
while IFS= read -r f; do
grep -q '^module gosqlite[.]org/' "$f" || continue
v=$(grep -oE 'gosqlite\.org v[0-9][^[:space:]]*' "$f" | awk '{print $2}' | head -n1 || true)
[ -n "$v" ] || continue
printf ' %-14s gosqlite.org %s\n' "$(dirname "$f")" "$v"
vers="${vers}${v}"$'\n'
done < <(find . -mindepth 2 -name go.mod -not -path './.*/*' -not -path './examples/*' | sort)
distinct=$(printf '%s' "$vers" | sed '/^$/d' | sort -u | wc -l | tr -d ' ')
if [ "$distinct" -gt 1 ]; then
echo "::error::published sub-modules pin different core versions"
exit 1
fi
echo "all published sub-modules pin the same gosqlite.org core version"
# Run gorm.io/gorm's own integration test suite against our dialector.
# The setup, reasoning, and current pass/fail counts live in
# dev/upstream/gorm.md; this job is the CI-enforced version of the
# recipe documented there. We pin gorm to the same version go.mod
# depends on, so the suite stays in lockstep with our `gorm/` package.
gorm_upstream:
name: gorm-upstream (v1.31.1)
runs-on: ubuntu-latest
env:
GORM_VERSION: v1.31.1
WORK_DIR: ${{ github.workspace }}/.gorm-upstream
steps:
- uses: actions/checkout@v5
with:
path: sqlite
# This job checks our repo out into ./sqlite (so gorm can be cloned
# into a sibling dir), so the local action lives under ./sqlite —
# not the workspace root, where every other job's checkout puts it.
# cache-dependency-path forwards to actions/setup-go so its
# toolchain cache restore finds our go.sum at the non-root checkout
# location; every other job inherits the default empty value.
- uses: ./sqlite/.github/actions/setup
with:
cache-dependency-path: sqlite/go.sum
# gorm's own unit tests (incl. the DDL parser) run in the `submodules`
# matrix above; this job stays focused on the upstream integration suite.
- name: Clone gorm at pinned version
run: |
mkdir -p "$WORK_DIR"
git clone --depth 1 --branch "$GORM_VERSION" \
https://github.com/go-gorm/gorm.git "$WORK_DIR/gorm"
- name: Write shim module re-exporting our dialector
# The upstream tests import gorm.io/driver/sqlite. We expose
# gosqlite.org/gorm under that path via a tiny shim,
# then append WAL + busy_timeout DSN flags so modernc's pure-Go
# file locks don't return SQLITE_BUSY on concurrent goroutines —
# mattn/go-sqlite3 (which gorm's tests were written against)
# serializes via libc-level locks instead.
run: |
mkdir -p "$WORK_DIR/shim"
cat > "$WORK_DIR/shim/go.mod" <<EOF
module gorm.io/driver/sqlite
go 1.25.0
require (
gosqlite.org/gorm v0.0.0
gorm.io/gorm $GORM_VERSION
)
replace gosqlite.org/gorm => ${{ github.workspace }}/sqlite/gorm
replace gosqlite.org => ${{ github.workspace }}/sqlite
EOF
cat > "$WORK_DIR/shim/sqlite.go" <<'EOF'
package sqlite
import (
"strings"
sqlitegorm "gosqlite.org/gorm"
"gorm.io/gorm"
)
const DriverName = sqlitegorm.DriverName
type (
Dialector = sqlitegorm.Dialector
Config = sqlitegorm.Config
)
const extraFlags = "_busy_timeout=5000&_journal=WAL&_sync=NORMAL"
func withFlags(dsn string) string {
if dsn == "" || strings.HasPrefix(dsn, ":memory:") || strings.Contains(dsn, "busy_timeout") {
return dsn
}
sep := "?"
if strings.Contains(dsn, "?") {
sep = "&"
}
return dsn + sep + extraFlags
}
func Open(dsn string) gorm.Dialector { return sqlitegorm.Open(withFlags(dsn)) }
func New(cfg Config) gorm.Dialector {
cfg.DSN = withFlags(cfg.DSN)
return sqlitegorm.New(cfg)
}
EOF
(cd "$WORK_DIR/shim" && go mod tidy)
- name: Wire shim into gorm/tests module
run: |
cat >> "$WORK_DIR/gorm/tests/go.mod" <<EOF
replace gorm.io/driver/sqlite => $WORK_DIR/shim
replace gosqlite.org/gorm => ${{ github.workspace }}/sqlite/gorm
replace gosqlite.org => ${{ github.workspace }}/sqlite
EOF
(cd "$WORK_DIR/gorm/tests" && go mod tidy)
- name: Run gorm upstream test suite
run: |
cd "$WORK_DIR/gorm/tests"
# tee duplicates output to a file we parse for PASS/FAIL counts
# below. Two pitfalls we work around here:
# 1. GitHub Actions prefixes every line with a UTC timestamp,
# even in the file written by tee — so we cannot anchor our
# grep with `^--- PASS:`. We use an unanchored match.
# 2. `go test`'s exit code is masked by tee's (always 0). We
# capture it via PIPESTATUS before the next command resets
# $?, and disable errexit around the pipeline so a
# legitimate test-fail doesn't abort our reporter.
set +e
go test -count=1 -timeout 5m -v ./... 2>&1 | tee /tmp/gorm-upstream.log
rc=${PIPESTATUS[0]}
set -e
# `grep -c` exits 1 on zero matches; `|| true` keeps the
# outer script alive so we can report the actual counts.
PASS=$(grep -c -- '--- PASS:' /tmp/gorm-upstream.log || true)
FAIL=$(grep -c -- '--- FAIL:' /tmp/gorm-upstream.log || true)
SKIP=$(grep -c -- '--- SKIP:' /tmp/gorm-upstream.log || true)
echo "--- summary ---"
echo "PASS=$PASS FAIL=$FAIL SKIP=$SKIP go-test-exit=$rc"
if [ "$rc" -ne 0 ]; then
echo "::error::go test exited with $rc"
exit "$rc"
fi
if [ "$FAIL" -ne 0 ]; then
echo "::error::gorm upstream suite reports $FAIL failures"
exit 1
fi
# PASS count includes sub-test lines (which are indented in the
# file too) — for gorm v1.31.1 the total is ~15000. Floor at
# 10000 to catch a "no tests ran at all" regression without
# being brittle on minor gorm bumps.
if [ "$PASS" -lt 10000 ]; then
echo "::error::gorm upstream PASS count regressed: got $PASS, expected >= 10000"
exit 1
fi