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
19 changes: 17 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -140,14 +140,29 @@ jobs:
run: make vet

# Uses the official golangci-lint GitHub Action, which handles binary
# download, caching, and version resolution automatically.
# Configuration is read from .golangci.yml at the repo root.
# download and caching. Configuration is read from .golangci.yml at the
# repo root.
#
# version is PINNED on purpose. Left unset the action resolves "latest",
# so every new golangci-lint release lands on unrelated PRs as a red CI
# run: v2.13.1 enabled the modernize/errorsastype analyzer and broke a
# branch that had touched none of the reported code. Bump this
# deliberately, in its own commit, with the fallout fixed alongside it.
#
# 2.13.1 is also a FLOOR, not just a pin: .golangci.yml excludes
# errors.AsType from errcheck by function name, and errcheck before
# 2.13.1 cannot resolve a generic function's name, so on an older
# golangci-lint that exclusion silently fails to match and every
# errors.AsType call site is reported. Keep docs/development.md's
# required-version note in step with this value.
#
# Explicit path patterns mirror the Makefile LINT_PKGS variable: they
# exclude web/node_modules/ (third-party JS packages that happen to
# contain Go code and are not part of the sqi codebase).
- name: golangci-lint
uses: golangci/golangci-lint-action@v9
with:
version: v2.13.1
args: --timeout=5m ./cmd/... ./internal/... ./pkg/... ./test/... ./web

# The internal/openjd/expr path-engine differentials shell out to python3
Expand Down
13 changes: 13 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,19 @@ linters:
errcheck:
check-type-assertions: true # catch unchecked x.(T)
check-blank: true
exclude-functions:
# errors.AsType[E](err) returns (E, bool). The first result is the
# extracted error value, not a failure signal — the bool is. Discarding
# it as `_, ok := errors.AsType[...](err)` is the idiomatic form when
# only the match matters, but check-blank sees a blank-assigned error
# and reports it. Exclude the function rather than turning check-blank
# off, so a genuine `_ = f()` is still caught.
#
# Requires golangci-lint >= 2.13.1: earlier errcheck cannot resolve a
# generic function's name (it reports "Error return value is not
# checked" with no name), so this entry never matches and every
# errors.AsType call site is flagged.
- errors.AsType

# gosec: suppress a few low-signal rules
gosec:
Expand Down
21 changes: 19 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ test-conformance: ## Run the official OpenJD conformance suite (needs the pinned
# unpinned upgrade could turn the differential test red without a single sqi
# commit — and because a divergence report is meaningless without knowing
# which build of the reference produced it.
OPENJD_MODEL_VERSION ?= 0.11.1
OPENJD_MODEL_VERSION ?= 0.11.4
ORACLE_VENV := .venv-oracle

.PHONY: expr-oracle-venv
Expand All @@ -237,7 +237,24 @@ test-expr-oracle: ## Differential-test the EXPR evaluator against the OpenJD ref
$(MAKE) --no-print-directory expr-oracle-venv || \
{ echo "could not install the reference implementation — skipping the expression oracle"; exit 0; }; \
fi
go test $(TEST_FLAGS) -tags oracle -run 'TestExprOracle' -v -timeout 5m ./test/oracle/
# An EXISTING venv is not evidence of the RIGHT venv: the guard above only
# creates one when it is missing, so before this check a raised
# OPENJD_MODEL_VERSION left the suite grading against the previous reference
# with nothing red to say so. Reinstall on mismatch, and let the test itself
# assert the version it actually spoke to (SQI_EXPR_ORACLE_EXPECT_VERSION) so
# the guarantee survives a hand-run `go test` too. Skipped entirely when
# SQI_EXPR_ORACLE_PYTHON points the harness at an interpreter we do not own.
@if [ -x "$(ORACLE_VENV)/bin/python3" ] && [ -z "$$SQI_EXPR_ORACLE_PYTHON" ]; then \
have=$$($(ORACLE_VENV)/bin/python3 -c \
'import importlib.metadata as m; print(m.version("openjd-model"))' 2>/dev/null); \
if [ "$$have" != "$(OPENJD_MODEL_VERSION)" ]; then \
echo "$(ORACLE_VENV) has openjd-model $$have, pin is $(OPENJD_MODEL_VERSION) — reinstalling"; \
$(MAKE) --no-print-directory expr-oracle-venv || \
{ echo "could not install the pinned reference implementation"; exit 1; }; \
fi; \
fi
SQI_EXPR_ORACLE_EXPECT_VERSION=$$([ -n "$$SQI_EXPR_ORACLE_PYTHON" ] || echo $(OPENJD_MODEL_VERSION)) \
go test $(TEST_FLAGS) -tags oracle -run 'TestExprOracle' -v -timeout 5m ./test/oracle/

# Validates the PUBLISHED preset library against the validator in this working
# tree. It exists because a validator change can silently invalidate content
Expand Down
14 changes: 13 additions & 1 deletion docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,22 @@ guides for extending the worker.
| Node.js ≥ 24 with npm ≥ 11 (see `.nvmrc` and `web/package.json` `engines`) | Build the web UI bundle embedded in `sqi-server` (`make build` runs it) | [nodejs.org](https://nodejs.org/) or `nvm use` |
| `gofumpt` | Stricter formatter (superset of `gofmt`) | `go install mvdan.cc/gofumpt@latest` |
| `goimports` | Import organizer | `go install golang.org/x/tools/cmd/goimports@latest` |
| `golangci-lint` | Linter suite | [golangci-lint.run/usage/install](https://golangci-lint.run/usage/install/) |
| `golangci-lint` ≥ 2.13.1 (CI pins this exact version; see below) | Linter suite | [golangci-lint.run/usage/install](https://golangci-lint.run/usage/install/) |
| `lefthook` | Git hook runner | `go install github.com/evilmartians/lefthook@latest` |
| `pkgsite` | Local pkg.go.dev docs server | `go install golang.org/x/pkgsite/cmd/pkgsite@latest` |
| Docker (optional) | Build and run the container image; also runs the real-directory LDAP tests (`make test-ldap`), the real-provider SSO tests (`make test-oidc`), and the real-root run-as-user isolation tests (`make test-isolation`), all of which skip cleanly without it | [docs.docker.com](https://docs.docker.com/get-docker/), or `brew install colima docker && colima start` |

`gofumpt`, `goimports`, and `golangci-lint` are required at commit time via
pre-commit hooks. Install them before running `make hooks`.

**`golangci-lint` 2.13.1 is a hard floor, not a suggestion.** `.golangci.yml`
excludes `errors.AsType` from `errcheck` by function name, and `errcheck` before
2.13.1 cannot resolve a *generic* function's name — it reports `Error return
value is not checked` with no name at all, so the exclusion never matches and
every `errors.AsType` call site in the repo is reported. On an older
golangci-lint `make lint` fails on code CI considers clean. CI pins the same
version in `.github/workflows/ci.yml`; keep the two in step when bumping.

---

## First-time setup
Expand Down Expand Up @@ -715,6 +723,10 @@ db := t.TempDir() + "/test.db"
[official instructions](https://golangci-lint.run/usage/install/). The
`go install golangci-lint` method is not supported by the project.

**`make lint` reports `Error return value is not checked` on `errors.AsType`**
— your `golangci-lint` predates 2.13.1. Check with `golangci-lint version` and
upgrade; see the version floor noted under [Prerequisites](#prerequisites).

**`gofumpt` or `goimports` not found after installing** — ensure `$(go env GOPATH)/bin`
is on your `$PATH`:
```sh
Expand Down
3 changes: 1 addition & 2 deletions internal/api/ws.go
Original file line number Diff line number Diff line change
Expand Up @@ -381,8 +381,7 @@ func (wc *wsConn) readLoop(ctx context.Context) {

// logReadError logs a WebSocket read error at the appropriate level.
func (wc *wsConn) logReadError(ctx context.Context, err error) {
var closeErr websocket.CloseError
if errors.As(err, &closeErr) {
if closeErr, ok := errors.AsType[websocket.CloseError](err); ok {
wc.logger.DebugContext(
ctx, "ws: client closed connection",
slog.Int("code", int(closeErr.Code)),
Expand Down
6 changes: 2 additions & 4 deletions internal/openjd/deadline_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -401,8 +401,7 @@ func TestCheckExpressionsAtSubmit_DeadlineIsNotASubmitValidationError(t *testing
if !errors.Is(serr, expr.ErrDeadlineExceeded) {
t.Fatalf("error = %v, want it to wrap expr.ErrDeadlineExceeded", serr)
}
var sve *SubmitValidationError
if errors.As(serr, &sve) {
if _, ok := errors.AsType[*SubmitValidationError](serr); ok {
t.Errorf("error = %v, want NOT a *SubmitValidationError: a wall-clock stop "+
"is not the submitter's fault", serr)
}
Expand Down Expand Up @@ -587,8 +586,7 @@ func TestSubmit_DeadlineIsNotASubmitValidationError(t *testing.T) {
if !errors.Is(err, expr.ErrDeadlineExceeded) {
t.Fatalf("error = %v, want it to wrap expr.ErrDeadlineExceeded", err)
}
var sve *SubmitValidationError
if errors.As(err, &sve) {
if _, ok := errors.AsType[*SubmitValidationError](err); ok {
t.Errorf("error = %v, want NOT a *SubmitValidationError: a wall-clock stop is "+
"the server giving up, not the submitter's fault, and that type is what "+
"internal/api turns into a 4xx", err)
Expand Down
Loading