Tooling
The tools each stack uses, their default settings, and how releases are built. usage.md lists the files each stack creates, and ci-baseline.md lists every workflow check.
Tools per stack
| Stack | Lint and format | Types | Tests | Packaging |
|---|---|---|---|---|
| Python | ruff |
ty |
pytest |
uv, locked, uv_build backend |
| TypeScript | biome |
tsc |
vitest, plus Playwright and axe for a web-app |
pnpm, plus Vite for a web-app |
| Rust | rustfmt, clippy |
rustc |
cargo nextest, plus cargo test --doc |
cargo, checked by cargo-deny |
| Go | golangci-lint (gofumpt, goimports) |
go vet, via golangci-lint |
go test -race, Examples included |
Go modules, checked by govulncheck |
Rust lints
Clippy lints live in [lints] in Cargo.toml. The policy enables clippy's pedantic group plus restriction lints against shortcuts: unwrap_used, expect_used, dbg_macro, todo, unimplemented, and allow_attributes_without_reason. unsafe_code is forbidden. Libraries also warn on missing_docs and prints to standard output or error.
clippy.toml permits unwrap, expect, and dbg! in tests. deny.toml limits dependencies to crates.io and to the licenses the project's license can take (ci-baseline.md).
Install cargo-nextest and cargo-deny once per machine. A Rust library's api job runs cargo-semver-checks against the last v* tag.
Go lints
Lints live in .golangci.yml for golangci-lint v2. The policy enables standard linters plus bodyclose, errorlint, gocritic, gosec, misspell, modernize, nilerr, noctx, revive, sloglint, unconvert, unparam, and usestdlibvars. gosec and noctx are disabled in tests. gofumpt and goimports handle formatting.
Install golangci-lint once per machine. The template pins the golangci-lint version, which copier update bumps. govulncheck is configured as a tool dependency in go.mod and requires no separate installation.
Git hooks
prek, a drop-in pre-commit replacement, runs the hooks. One .pre-commit-config.yaml covers all stacks. Python projects install prek with uv tool install prek; TypeScript-only projects use the @j178/prek devDependency.
| Area | Hooks |
|---|---|
| Every project | file hygiene, check-toml, committed (commit messages), typos, lychee, rumdl (Markdown), betterleaks (secrets), shellcheck, yamlfmt, yamllint, jsonvet, zizmor, jactionlint |
| Public or licensed | dco-signoff (public), reuse (licensed) |
| Citable | check-citation-file-format, citesync |
| Python | ruff-check, ruff-format, ty, nbstripout, sp-repo-review |
| TypeScript | biome, tsc, and vitest in a web app |
| Rust | cargo-fmt, cargo-clippy |
| Go | golangci-lint-fmt, golangci-lint |
| Python image | hadolint-docker, which needs a running Docker daemon |
Spelling and link checks
Two git hooks validate prose:
typos: detects spelling errors in code, comments, and documentation. Add project-specific terms to_typos.tomlat the repository root:
[default.extend-words]
ue = "ue"
typos knows English only. The hook skips gettext .po files and anything in a locales/ directory; list other files with text in other languages under [files] extend-exclude in _typos.toml.
lychee: validates links. The local git hook runs offline and checks relative paths. The weekly workflow.github/workflows/links.ymlchecks both internal and external web links. In a web app, both readlychee.toml, which resolves root-relative links such as/favicon.svgin the Vitepublic/directory.
Documentation rules
- Full URLs in package READMEs: PyPI, npm, and crates.io render the project's
README.mdon their package pages. Relative links break when viewed on registry sites, so links to repository files and directories use absolute GitHub URLs (https://github.com/<owner>/<repo>/...). A render withpublish_to_pypiorpublish_to_npmwrites its README links this way. - Stable links to documentation on
main: When documentation or CLI error messages link to rules or guides, point tomain. Rule identifiers are permanent; if a rule is deprecated or removed, keep a stub heading so historical links continue to resolve. - Single source of truth: The README holds the overview and quickstart; code comments and docstrings define API contracts;
docs/holds extended guides and references.
Tasks
just runs the tasks. The root justfile imports a just/<stack>.just module per stack. Recipes carry a stack prefix (py-, ts-, rs-, go-, and docker- in a Compose-only repo). The exceptions are kind-specific: fetch, build, validate and pipeline (data-pipeline), serve (web-service) and up (docker).
just check runs the checks of every enabled stack, then the local-check recipe in just/local.just if there is one. In a Compose-only repo it validates the Compose file and lints the shell scripts.
Coverage is opt-in. just py-cov and just ts-cov print summaries without threshold gates. With the codecov extra, CI uploads coverage to Codecov, and just check runs the TypeScript tests with coverage.
Documentation site
With the docs_site extra, Zensical builds docs/ into a site, and mkdocstrings renders a Python library's API reference.
Docker images
The Dockerfile follows upstream uv patterns: multi-stage builds on -slim base images, BuildKit cache mounts, a non-root runtime user, and OCI labels.
Dependencies
Generated projects use Dependabot by default. dependency_bot=renovate writes equivalent rules to renovate.json5. Both bots follow these policies:
- Weekly version updates grouped into three PRs: actions and hooks, dev tooling, and minor/patch runtime updates.
- Runtime major versions get separate individual PRs.
- Updates wait one week before opening, majors included.
- Security updates bypass the cooldown.
Dependabot also combines npm security updates into one PR. Renovate refreshes lockfiles weekly on Monday.
uv's exclude-newer = "1 week" and pnpm's minimumReleaseAge hold back local installs to match the bot cooldown. Go has no equivalent cooldown.
To lock an urgent security patch before the cooldown expires, add a temporary exemption:
- uv: add the package under
[tool.uv]inpyproject.tomlwith a timestamp after the patch release, then runuv lock --upgrade-package <pkg>:
exclude-newer-package = { certifi = "2026-10-21T00:00:00Z" }
Do not rely solely on uv lock --exclude-newer-package <pkg>=<timestamp>; later uv sync calls reset it. Remove the entry once the patch is a week old.
- pnpm: run
pnpm audit --fix=update. This adds an entry underminimumReleaseAgeExcludeinpnpm-workspace.yaml. You can also add it manually and runpnpm install:
minimumReleaseAgeExclude:
- [email protected]
minimumReleaseAgeExcludePrune removes the entry automatically once the lockfile moves past that version.
The Docker base image tag (python_version) is a Copier answer. Rebuilds pull the latest security patches for that tag.
Fresh renders generate without lockfiles. The initial uv lock or pnpm install locks the newest versions past the one-week cooldown. Once committed, CI installs strictly from lockfiles.
Both just check and CI run dependency audits:
uv auditfor Python dependencies.pnpm audit --prodfor production TypeScript packages. Astro site workflows usepnpm auditto include build tools.cargo deny checkfor Rust crates and licenses.govulncheckfor Go code paths.- A license check per stack, unless
license=none: licensecheck,pnpm run licenses,cargo deny listand go-licenses. An Astro site runs the same production dependency check as TypeScript duringjust site; Astro itself is build tooling indevDependencies. Each checks only the dependencies that ship: Python's runtime dependencies without dependency groups, TypeScript's--prodpackages, Rust without dev-dependencies, and the Go packages a build imports, not test-only ones. TypeScript, Rust and Go refuse an LGPL-2.1-only dependency in every project, since GPL-3.0 cannot take it; Go refuses every LGPL-2.1 dependency, because go-licenses cannot tell -only from -or-later. Python follows licensecheck's own table, which lets LGPL-2.1 in. - Knip for unused TypeScript files, exports, and packages.
Release workflows
Release workflows separate artifact build from artifact publication:
- Build: re-runs checks and tests the packaged artifact directly. Installs the Python wheel into a clean environment to run tests, or verifies npm packages with
publint. - Publish: downloads the built artifact and uploads it using OpenID Connect (
id-token: write). This job runs no project code.
With PyPI publishing, a final job attaches wheels, a CycloneDX SBOM and provenance bundles to the GitHub release, after gh attestation verify passes on each. Images pushed to GHCR include an SBOM and a signed provenance attestation.