sloff

module
v0.0.9 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 31, 2026 License: MIT

README ΒΆ

sloff πŸ¦₯

A fingerprint-aware codegen orchestrator for polyglot monorepos. Skips work when inputs haven't changed β€” and never lies about it.

sloff runs your code generators (proto / SQL / mock / GraphQL / etc.) and fingerprints their outputs in git so devs and CI skip re-generation when nothing has changed. Fingerprint hits are validated by comparing both inputs and the actual output files against the recorded state β€” so even when the fingerprint looks valid, a drifted output triggers re-generation rather than a stale skip.

Features

  • Output-comparison fingerprint hits. A hit requires the recorded input_hash and the on-disk output files to match. Drifted outputs (manual edits, formatter runs, partial checkouts) are re-generated, never silently skipped.
  • OS-portable fingerprints. Fingerprints are deterministic protobuf binary records committed to git (inspect them with sloff fingerprint show). A record built on macOS works on Linux CI without rebuilds β€” tool versions are captured as logical strings, never as OS-specific binary hashes.
  • Reads your existing toolchain. No replacement for aqua / mise / nix / pnpm / go.mod. Tool versions come from the runtime binary's --version, lockfiles, or repo source β€” whichever is the actual source of truth.
  • Explicit, validated dependencies. Execution order comes from declared depends edges, so it is deterministic even on a freshly cleaned tree. Declarations are cross-checked against observed inputs / outputs overlap β€” reading another task's outputs without declaring the edge is an error, so the DAG can't silently drift from reality.
  • Cold-state bootstrap. A tool whose sources import generated files declares its producers once, on the tool (tools.<name>.depends); resolution defers until they've run, so sloff run succeeds in one shot even after deleting every generated file.
  • Dynamic task sets. command_providers run at plan time and emit tasks as JSON β€” per-directory fan-out and import-closure inputs stay out of hand-written YAML, and generated tasks flow through the same validation and fingerprinting as static ones.
  • Single Go binary. No runtime dependencies, no daemon, no language ecosystem to install.
  • Codegen-only by design. Build / test / lint stay in your existing tooling (Make, npm scripts, etc.) β€” sloff does one thing.

Install

go install github.com/izumin5210/sloff/cmd/sloff@latest

Quick start

Place a sloff.yml in any directory containing your codegen inputs:

tools:
  buf:
    exec: ["buf", "--version"]
  protoc-gen-go:
    exec: ["protoc-gen-go", "--version"]
    extract: 'v[0-9]+\.[0-9]+\.[0-9]+'

commands:
  - name: protoc-gen-go
    cmd: buf generate
    inputs: ["**/*.proto", "buf.gen.yaml", "buf.yaml", "buf.lock"]
    outputs: ["**/*.pb.go", "**/*.connect.go"]
    tools: [buf, protoc-gen-go]

Then from anywhere in the repo:

sloff run

sloff run discovers every sloff.yml, orders tasks by their declared depends edges, and either skips or re-runs each task based on fingerprint lookup.

Task dependencies

When a task consumes files that another task generates, declare the edge with depends. Each entry names a task in the same file, or in another spec dir (relative to the declaring sloff.yml):

commands:
  - name: bundle
    cmd: ./bundle.sh
    inputs: ["../gen/**/*.pb.ts"]
    outputs: ["dist/bundle.ts"]
    tools: [bundler]
    depends:
      - { spec: ../gen, task: codegen } # task in another spec dir
      - { task: lint-schema }           # task in this file

depends only controls scheduling β€” invalidation still flows through file contents (when an upstream output listed in your inputs changes, your fingerprint changes). To keep the two honest, sloff validates declarations against observed inputs / outputs overlap:

  • Reading files another task produces without declaring depends on it β†’ error, with the missing edge spelled out.
  • Declaring depends on a task whose outputs never appear in your inputs β†’ warning (that dependency will never invalidate you).
Pattern dependencies

task accepts glob patterns, expanded at plan time against the target spec's task set β€” including dynamically generated tasks:

    depends:
      - { spec: ../gen, task: "gen-*" } # every gen-* task, present and future
Barrier tasks

A barrier: true task is a pure aggregation point: it executes nothing, has no fingerprint, and completes when all of its depends complete (failing if any of them fails). Use it to give "these N tasks are done" a single name:

commands:
  - name: gen-all
    barrier: true
    depends:
      - { task: "gen-*" }

Barriers declare only depends β€” cmd / inputs / outputs / tools are rejected. Depending on a barrier is not a substitute for data edges: a task that actually reads a member's outputs still needs a direct depends on that producer.

Tool bootstrap dependencies

When a tool's own sources depend on generated files (e.g. an in-repo protoc plugin that imports generated *.pb.go), declare that on the tool instead of repeating it on every consumer task:

tools:
  protoc-gen-foo:
    go-local: ./cmd/protoc-gen-foo
    depends:
      - { task: gen-options } # the task that generates what the tool imports

sloff injects the edge into every task that uses the tool. On a clean tree β€” where the tool can't even be resolved until those files exist β€” resolution is deferred until its declared dependencies have run, so a single sloff run bootstraps from zero. Tools without a depends declaration keep failing fast at run start.

Dynamic tasks

When the task set itself is derived from your tree (per-directory codegen, import-closure inputs), declare a command_providers entry instead of generating sloff.yml files out-of-band:

command_providers:
  - name: proto-perdir
    exec: ["go", "run", "./tools/emit-proto-tasks"]

The provider runs at plan time (cwd = the spec dir) and prints the task list as JSON on stdout:

{
  "schema_version": "v1",
  "tasks": [
    {
      "name": "gen-foo",
      "cmd": ["buf", "generate", "--path", "foo"],
      "inputs": ["foo/**/*.proto"],
      "outputs": ["gen/foo/**/*.pb.go"],
      "tools": ["buf"]
    }
  ]
}

Generated tasks go through exactly the same validation, dependency checks, and fingerprinting as hand-written ones, and providers re-run on every sloff run β€” the task set can't drift from the tree.

Tool resolvers

tools: entries dispatch to one of three resolvers based on shape:

script β€” for prebuilt binaries

For anything with a --version command: aqua / mise / nix-distributed CLIs, go tool-managed bins, npm bins via pnpm exec, etc. The runtime binary's stdout is the version source of truth.

tools:
  buf:
    exec: ["buf", "--version"]
  protoc-gen-go:
    exec: ["protoc-gen-go", "--version"]
    extract: 'v[0-9]+\.[0-9]+\.[0-9]+'
go-local β€” for repo-internal Go CLIs

For codegen tools you maintain in this repo as Go cmd/... packages, run via go run:

tools:
  protoc-gen-foo:
    go-local: ./cmd/protoc-gen-foo

Internal .go source files contribute to the task's effective inputs (so source edits invalidate); external Go module versions come from go.sum.

pnpm-local β€” for pnpm workspace internal packages

For codegen tools you maintain as pnpm workspace packages:

tools:
  codegen:
    pnpm-local: "@org/codegen"

commands:
  - name: gen
    cmd: ["sh", "-c", "pnpm --filter @org/codegen build && pnpm exec my-codegen"]
    inputs: ["**/*.proto"]
    outputs: ["**/*.pb.ts"]
    tools: [codegen]

git-tracked files in the workspace package (and transitive workspace deps) contribute to inputs; external npm dep versions come from pnpm-lock.yaml. Build steps stay in the task cmd.

Fingerprint storage

By default sloff persists records to .sloff/fingerprints/ under the repo root and commits them to git (ADR-0003). For monorepos where git-noise / clone-size pressure starts mattering, switch to the DynamoDB backend via .sloff/config.yml:

# .sloff/config.yml
fingerprint:
  backend: dynamodb
  dynamodb:
    table: sloff-fingerprints # required
    region: us-east-1         # optional; falls back to AWS_REGION / shared config
    # endpoint: ""            # optional; emulator URL
    # expires_after_days: 0   # optional; >0 enables DynamoDB TTL-based GC

Credentials are resolved through the aws-sdk-go-v2 default chain (env vars / ~/.aws/credentials / IRSA / IMDS), so the config file carries no secrets and is safe to commit. The DynamoDB backend is fronted by a transparent $XDG_CACHE_HOME-rooted disk cache so warm lookups stay local; see Storage: DynamoDB for the full design (schema, caching, consistency, cost).

sloff does not auto-create the DynamoDB table β€” provision it once with your IaC of choice.

Table provisioning (AWS CLI / Terraform)
aws dynamodb create-table \
  --table-name sloff-fingerprints \
  --attribute-definitions \
      AttributeName=pk,AttributeType=S \
      AttributeName=sk,AttributeType=S \
  --key-schema \
      AttributeName=pk,KeyType=HASH \
      AttributeName=sk,KeyType=RANGE \
  --billing-mode PAY_PER_REQUEST
resource "aws_dynamodb_table" "sloff_fingerprints" {
  name         = "sloff-fingerprints"
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "pk"
  range_key    = "sk"

  attribute { name = "pk"; type = "S" }
  attribute { name = "sk"; type = "S" }

  # Required only when expires_after_days > 0 in .sloff/config.yml.
  ttl {
    attribute_name = "expires_at"
    enabled        = true
  }
}
Required IAM actions
{
  "Effect": "Allow",
  "Action": [
    "dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:DeleteItem",
    "dynamodb:Query", "dynamodb:Scan",
    "dynamodb:BatchGetItem", "dynamodb:BatchWriteItem",
    "dynamodb:DescribeTable"
  ],
  "Resource": "arn:aws:dynamodb:<region>:<account>:table/sloff-fingerprints"
}

CLI reference

Command What it does
sloff run Discover specs and run / skip every task. --force re-executes everything while still writing records; --root / --pattern scope spec discovery.
sloff graph Render the declared task DAG as Mermaid (default) or DOT (--format dot).
sloff fingerprint show <file> Decode a fingerprint record to JSON β€” also works as a git diff textconv.
sloff fingerprint diff <a> <b> Semantic diff between two records (exit code 1 if they differ).
sloff fingerprint gc Collapse duplicate record variants left behind by branch merges.
sloff version Print the binary version.

Environment variables:

  • SLOFF_ALLOW_STALE_DEPS=1 β€” degrade preflight failures (e.g. pnpm install drift) from a hard error to a warning; the run proceeds but fingerprints are not written for a known-suspect run.
  • SLOFF_NO_FILE_HASH_CACHE=1 β€” skip the persistent per-file digest cache and rehash everything from disk.

sloff also emits OpenTelemetry trace spans (per-phase and per-task timing, fingerprint hit/miss) when the standard OTEL_* env vars are set β€” nothing is exported unless you set OTEL_EXPORTER_OTLP_ENDPOINT / OTEL_TRACES_EXPORTER. Every OTEL_* key can be overridden just for sloff via a SLOFF_OTEL_* twin.

When to use sloff (vs alternatives)

sloff is well-suited when:

  • You have a polyglot codegen pipeline (Go + JS/TS + external prebuilt binaries) and want shared fingerprint store across devs and CI.
  • You want to keep your existing toolchain (aqua / mise / nix / pnpm / go.mod) instead of migrating to a new build system.
  • You're OK with outputs being committed to git (no separate artifact cache infrastructure).

You probably want a different tool when:

Need Better fit
Cache compile artifacts and binaries (not just codegen outputs) Bazel / Buck2
Hermetic build with full toolchain isolation Bazel / Buck2
General task runner (build / test / lint / dev server / formatter) moonrepo / Nx
JS/TS-only monorepo Turborepo / Nx
File-grained dependency inference Pants
Battle-tested at massive scale Any of the above

sloff is intentionally narrow β€” codegen orchestration with honest fingerprints, nothing else.

Documentation

License

MIT

Directories ΒΆ

Path Synopsis
cmd
sloff command
Command sloff is the fingerprint-aware codegen orchestrator CLI.
Command sloff is the fingerprint-aware codegen orchestrator CLI.
internal
sloff/depgraph
Package depgraph builds the task DAG and emits a stable topological order.
Package depgraph builds the task DAG and emits a stable topological order.
sloff/explain
Package explain projects depgraph tasks into their declared dependency edges (ADR-0013) plus the file-overlap evidence observable for each edge.
Package explain projects depgraph tasks into their declared dependency edges (ADR-0013) plus the file-overlap evidence observable for each edge.
sloff/fingerprint
Package fingerprint defines the storage interface and serialization helpers for sloff's on-disk fingerprints.
Package fingerprint defines the storage interface and serialization helpers for sloff's on-disk fingerprints.
sloff/fingerprint/cached
Package cached is a fingerprint.Storage decorator that mirrors records to a host-local directory under XDG_CACHE_HOME.
Package cached is a fingerprint.Storage decorator that mirrors records to a host-local directory under XDG_CACHE_HOME.
sloff/fingerprint/dynamodb
Package dynamodb is a fingerprint.Storage backend that persists records as individual items in an Amazon DynamoDB table.
Package dynamodb is a fingerprint.Storage backend that persists records as individual items in an Amazon DynamoDB table.
sloff/fingerprint/local
Package local is the default Storage backend that persists records as protobuf binary files on the local filesystem under <repoRoot>/.sloff/fingerprints/.
Package local is the default Storage backend that persists records as protobuf binary files on the local filesystem under <repoRoot>/.sloff/fingerprints/.
sloff/glob
Package glob expands inputs/outputs patterns declared in a sloff.yml.
Package glob expands inputs/outputs patterns declared in a sloff.yml.
sloff/hash
Package hash computes the deterministic hash inputs that drive sloff's fingerprint lookup.
Package hash computes the deterministic hash inputs that drive sloff's fingerprint lookup.
sloff/preflight
Package preflight verifies that build artefacts and source state are mutually consistent before the runner trusts the fingerprint.
Package preflight verifies that build artefacts and source state are mutually consistent before the runner trusts the fingerprint.
sloff/preflight/pnpmlocal
Package pnpmlocal implements the preflight Checker that protects pnpm-local from running against a stale node_modules.
Package pnpmlocal implements the preflight Checker that protects pnpm-local from running against a stale node_modules.
sloff/provider
Package provider expands command_providers (ADR-0015): programs the runner execs at plan time whose stdout is a versioned JSON envelope of task definitions.
Package provider expands command_providers (ADR-0015): programs the runner execs at plan time whose stdout is a versioned JSON envelope of task definitions.
sloff/runner
Package runner orchestrates spec discovery, preflight, declared-dependency DAG construction and per-task fingerprint lookup/execute/write.
Package runner orchestrates spec discovery, preflight, declared-dependency DAG construction and per-task fingerprint lookup/execute/write.
sloff/spec
Package spec provides parsers for sloff.yml task specs and the repository-wide tool registry that named tools[] references resolve against (ADR-0008).
Package spec provides parsers for sloff.yml task specs and the repository-wide tool registry that named tools[] references resolve against (ADR-0008).
sloff/timing
Package timing turns the OpenTelemetry spans the runner already emits into a human-readable, run-end phase/task breakdown printed to stderr.
Package timing turns the OpenTelemetry spans the runner already emits into a human-readable, run-end phase/task breakdown printed to stderr.
sloff/toolresolver
Package toolresolver dispatches tool version resolution to per-channel resolvers (script for prebuilt binaries β€” including external npm / Go OSS packages, see ADR-0007 β€” and go-local / pnpm-local for internal sources) and produces the OS-neutral logical version strings that feed the fingerprint's resolved_versions_hash component, plus the ExtraInputs that feed the runner's depgraph derivation.
Package toolresolver dispatches tool version resolution to per-channel resolvers (script for prebuilt binaries β€” including external npm / Go OSS packages, see ADR-0007 β€” and go-local / pnpm-local for internal sources) and produces the OS-neutral logical version strings that feed the fingerprint's resolved_versions_hash component, plus the ExtraInputs that feed the runner's depgraph derivation.
sloff/toolresolver/golocal
Package golocal implements toolresolver.Resolver for repo-local Go tools.
Package golocal implements toolresolver.Resolver for repo-local Go tools.
sloff/toolresolver/lister
Package lister enumerates the source contributions that feed the go-local resolver's hash.
Package lister enumerates the source contributions that feed the go-local resolver's hash.
sloff/toolresolver/pnpmlocal
Package pnpmlocal implements toolresolver.Resolver for pnpm workspace-local tools.
Package pnpmlocal implements toolresolver.Resolver for pnpm workspace-local tools.
sloff/toolresolver/script
Package script implements toolresolver.Resolver for the prebuilt-binary channel: it runs a user-declared command (typically <bin> --version), captures stdout, optionally applies an extract regex, and returns the resulting string as the OS-neutral logical version.
Package script implements toolresolver.Resolver for the prebuilt-binary channel: it runs a user-declared command (typically <bin> --version), captures stdout, optionally applies an extract regex, and returns the resulting string as the OS-neutral logical version.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL