release v0.12.0: pin intra-repo gosqlite.org requires to v0.12.0 #39
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |