Contributing to xtandard-image
Thank you for helping. This file covers how to set up a checkout, which checks must pass and what a pull request needs. Bug reports and feature requests go through the issue templates. Never report a security problem in a public issue. SECURITY.md explains how to report it privately.
By taking part you agree to the Code of Conduct. Contributions are accepted under the project’s MIT licence. There is no CLA, and no DCO sign-off is needed.
What the project is
Section titled “What the project is”xtandard-image is a server: a Go 1.27 program on libvips (through cgo), shipped as Docker images in three flavours: slim, full (adds FFmpeg LGPL and ONNX Runtime) and gpl (opt-in, contains GPL and AGPL software). The packages in clients/, including @xtandard/image on npm, are clients of that server and are generated from its catalogues. Most changes touch the server. Client changes usually start with a server change too.
Read these before writing code:
docs/go-standards.mdis binding for Go code: toolchain, HTTP, concurrency, cgo, errors, configuration, tests and dependencies. Its §10 lists the banned modules, anddepguardenforces them.docs/ARCHITECTURE.mdcovers the request lifecycle, the packages and the interfaces between them.docs/adr/holds the decisions. Do not reopen one in a pull request without first opening an issue.- The user docs your change affects:
CONFIG.md,URL-DIALECTS.md,FORMATS.mdandSOURCES.md.
Development setup
Section titled “Development setup”You need Git, Docker (Docker Desktop, OrbStack or a Linux engine, with Compose v2) and mise. mise installs the pinned toolchain from .mise.toml: Go 1.27.1, golangci-lint 2.14.0, Bun 1.4.2 and Task.
git clone https://github.com/xantiagoma/xtandard-image.gitcd xtandard-imagemise trust && mise installtask --list # every task, with a descriptionBuild and test inside Docker (canonical)
Section titled “Build and test inside Docker (canonical)”The product is the Docker image, so the reference environment is the image’s builder stage. It carries libvips 8.18 and every C library the server links, plus Go and golangci-lint. The docker:* tasks mount your checkout into that image (compose.dev.yml):
task docker:lint # golangci-lint fmt/run, go vet, go fix -diff, go mod tidy -diff, cgo placementtask docker:test # go test -race -shuffle=on ./...task docker:shell # a shell in the builder, repository at /srcBy default they use the published builder, ghcr.io/xantiagoma/xtandard-image-base:slim-builder. To test the video and detection code, point XI_BASE_IMAGE at a full or gpl builder. The tasks then add -tags ffmpeg. If you cannot pull the published base, or you are changing docker/base, build it locally for your architecture:
task docker:base:slim # xtandard-base:dev and xtandard-base:dev-builder (slow: libvips and its codecs from source)XI_BASE_IMAGE=xtandard-base:dev-builder task docker:testtask docker:base:full && task docker:base:gpl # optional: the other flavours, built on top of slimThe app image and the end-to-end suites:
task release:build # the app image, xtandard-image:devXI_APP_BASE=xtandard-base:dev task release:build # the same image, on a base you built locallytask release:verify && task smoke && task acceptance # image contract, smoke test, acceptance suitetask quickstart # runs the README quick start as writtentask test:integration # -tags integration against compose.test.yml (S3, Azurite, FTP, SFTP, Redis…)On the host
Section titled “On the host”A host toolchain is fine for fast iteration. Pure-Go packages run with CGO_ENABLED=0 (task test:nocgo). For the cgo packages on macOS, run brew install vips pkg-config and export CGO_CFLAGS_ALLOW=-Xpreprocessor. Homebrew’s libvips is a different build from the image’s, so a host-only pass does not count as verification. Run the Docker tasks before you open a pull request.
TypeScript, the playground and the clients
Section titled “TypeScript, the playground and the clients”bun installtask ts:check # @xtandard/image: generated-types drift, typecheck, tests, licences, packtask playground:test # the playground's JavaScript unit teststask clients:check # spec.json, every client's generated files and the code generator agreetask clients:test # each client's own `task ci`; toolchains come from clients/<lang>/mise.tomlChecks a pull request must pass
Section titled “Checks a pull request must pass”CI runs all of these, and they must pass before you ask for a review. Run them in the builder (task docker:lint, task docker:test) or with the same versions on the host:
golangci-lint fmt --diff && golangci-lint run ./... # the v2.14.0 binary, not `go tool`go vet ./...go fix -diff ./... # must print nothinggo mod tidy -diffgo test -race -shuffle=on ./...go tool -modfile=tools/go.mod govulncheck ./...Add task ts:check and task clients:check when you touch TypeScript, the OpenAPI document, the option catalogues or clients/. Add task quickstart when you touch the README quick start.
Rules that come up in review
Section titled “Rules that come up in review”These are short versions of CLAUDE.md and docs/go-standards.md. Those two files are the full rules.
- Layout. All Go code lives under
internal/, andcmd/xtandard-image/main.goonly wires things together. Onlyinternal/vipsimports"C". The one exception isinternal/video/ffmpeg, behind theffmpegbuild tag (ADR 0010). No C type leaks out of those packages, andscripts/check-cgo.shchecks it. - Configuration. Configuration comes only from
internal/config: oneConfigstruct andXI_*variables, with noos.Getenvanywhere else, no globals and noinit(). A new setting goes indocs/CONFIG.mdand the JSON Schema in the same pull request. - Errors and logs. Wrap errors with
%w. Map them to HTTP status codes in one place, and never send internal error text to clients. Log withlog/slogJSON: static messages andsnake_casekeys. - Tests. Use the stdlib
testingpackage andgo-cmp, in table-driven tests witht.Parallel(). Usesynctestinstead oftime.Sleep. Every parser and decoder gets a fuzz target. Compare golden images perceptually, never byte for byte. - Performance claims need
for b.Loop()benchmarks andbenchstatoutput from-count=10runs. - Dependencies need an ADR. A new Go module, C library, base image package or client runtime dependency needs a line in the relevant ADR, with the reason and its licence. Copied or derived code, as from imgproxy, keeps its upstream header and gets an entry in
NOTICE. - Licence policy (ADR 0006). Go modules must be MIT, BSD, Apache-2.0 or ISC. The
slimandfullimages contain no GPL or AGPL code; LGPL is allowed when dynamically linked. GPL and AGPL software goes only in the opt-ingplflavour, and only the components its amendments list. Test fixtures need a licence entry intestdata/LICENSES.md. - Clients are generated. The option tables of every client come from
clients/spec/spec.json.clients/codegenproduces them through a code model and a pretty-printer, never by string concatenation or templates. Do not edit generated files by hand. After changing a Go option, runtask clients:spec clients:generate, thenbun run generateinclients/typescript, and commit the result (clients/README.md). A new language follows “Adding a language” there. - Docs describe what is on
main. Do not document planned behaviour as if it worked. A behaviour change updates the user docs and, if users will notice it, theUnreleasedsection ofCHANGELOG.md. - Docs are the website. The documentation website is built from the Markdown in the repository (ADR 0015). Keep links relative, as GitHub renders them; the build turns them into site routes and fails on a broken link or anchor. A new file under
docs/needs a place insite/manifest.ts. Preview withtask pages:dev, check withtask pages:build.
Commits and pull requests
Section titled “Commits and pull requests”- Conventional Commits:
type(scope): imperative subject. Types:feat,fix,docs,test,refactor,perf,build,ci,choreanddeploy. The scope is the package or area:fix(server): …,feat(dialect/imgix): …,docs(config): …,feat(clients/python): …. Mark a breaking change with!(feat(config)!: rename …) and explain it in the body. Older history often uses just the area (server: …). Use the full form for new commits. - Keep commits small and focused. A pull request does one thing. Larger work is split into work packages in
docs/ROADMAP.md. Open an issue first for anything big, so we can agree on the approach before you write it. - Fill in the pull request template: what changed, why, how you verified it (paste the summary of the checks above), and any deviations from the docs or ADRs.
- A change that breaks configuration, URL options or a client API ships in the next minor version (versioning). Say so in the pull request, and add an entry to the changelog.
Reference implementations
Section titled “Reference implementations”The project reimplements other services’ URL dialects and options. Check the real implementation instead of guessing: scripts/clone-refs.sh makes shallow clones of imgproxy, imagor, thumbor, imaginary, weserv, unpic, the provider SDKs, libvips, sharp and LibRaw in .refs/, which git ignores. Search them with rg before you implement a dialect, an option or a vips call.