Hi, and thanks for considering a contribution to scampi!
scampi is a small, solo-developed project. PRs and issues are very welcome, but a few notes will save us both some friction.
Pick an issue template that matches what you're filing:
- Bug — something is broken or behaves wrong
- Feature — you want scampi to do something it currently doesn't
If neither fits (a question, a vague idea, a "should we?"), file a plain issue and we'll figure out the right shape together.
For security issues, please don't file a public issue —
SECURITY.md has the disclosure path.
A PR template will pre-fill a few sections (summary, linked issue, test plan, checklist). Fill them in honestly; "test plan: not sure how to test this" is a fine answer that opens a conversation.
A few load-bearing expectations:
- Link an issue. New PRs without an issue tend to stall while we figure out whether the change is wanted. Open the issue first if there isn't one.
- One concern per PR. Combined "fix bug + refactor module + tweak unrelated lint" PRs are hard to review and harder to revert.
- Tests when reasonable. Behavior changes usually want an
integration test under
test/. Pure-function tweaks usually want a small unit test next to them. "I didn't add tests because X" is also a reasonable answer if X is honest.
You'll need Go (the version in go.mod or newer) and just
on your PATH. The repo is a single Go module — go build ./...
works, but just recipes do the right thing without you having to
remember flags.
just build # build the scampi binary to ./build/bin/
just test fast # fast tests, no containers, no race detector
just lint # golangci-lint + codebase invariant rules
just fmt # format Go + markdown tablesTwo test tiers:
just test fast # per-commit gate, run before every commit (with just lint)
just test full # complete run — race + containers + testkit + bench smokeCI runs full. Hitting fast locally before every commit catches
regressions where the feedback loop is tight rather than waiting for CI
to fail.
just test (bare) shows the full list of test recipes.
The mechanical bits are enforced by just lint — that's the source
of truth, no need to memorise rules. A couple of conventions the
linter doesn't catch:
- Comments earn their place by explaining why, not what. Don't restate what the code already says.
- Section banners look like this:
// Title // -----------------------------------------------------------------------------
- Naming — the project's terminology is StepKind, Step, Op,
Target; the linker emits
Declared*types (DeclaredConfig,DeclaredStep, …), the engine runs the bare execution nouns. AvoidImpl,Handler,Spec, andInstance/Typesuffixes. - Test names are
Test_Subject_Expectation, where the expectation states the asserted outcome with a verb:Test_SSH_RejectsWrongKey, notTest_SSH_ConnectWrongKey.just lintenforces the shape and the verb vocabulary.
If you're unsure whether a change fits the project style, file the issue first and we'll talk it through before you write code.
Short, title-only, no body. We use GitHub magic keywords in parentheses at the end:
feat(std): @size attribute for human-readable byte amounts (closes #250)
fix(lang/format): emit attributes on type-decl fields (refs #244)
(closes #N)for everything that resolves an issue(fixes #N)for bug fixes specifically (labelkind/bug)(refs #N)if the commit relates to an issue but doesn't close it
<type> matches conventional commits (feat, fix, docs, chore,
refactor, test, ci). Scopes are package-shaped ((std),
(lang/format), (engine)).
No commit bodies, no signed-off-by.
If you're adding a new step kind:
- Create
internal/step/<kind>/<kind>.goimplementing thespec.StepKindinterface. - Add a config struct with
step/summary/optional/default/examplefield tags. - Register in
internal/engine/registry.go. - Cover it with an E2E scenario under
test/testdata/e2e/exercising the full plan → check → apply pipeline. - Update the site documentation under
site/content/.
scampi is opinionated. The feature surface is intentionally narrow — "batteries included" steps over plugin sprawl, deterministic fail-fast execution over best-effort orchestration, the project's own language over YAML/HCL templating.
A well-written PR can still get a "not in scope" response. Examples of what's likely to bounce:
- Plugin / extension APIs. New step kinds belong in the standard library, not behind a runtime extension point.
- Alternative config formats alongside scampi (YAML, HCL, templating layers). One language, one mental model.
- Best-effort retry / recovery that masks underlying failures. scampi prefers fail-fast with a clear diagnostic over silent partial success.
- Direct ports of step implementations from other tools without rethinking the model in scampi terms.
If you're unsure, file the issue first. A short discussion before you spend time coding is the cheapest possible alignment.
File the issue, send the PR, ask the question. The worst that can happen is "no, but here's why," and even that's useful information for both of us.
For questions that don't fit issues, my email is on my GitHub profile.