Skip to content

Contributing

Jake Paine edited this page May 23, 2026 · 5 revisions

Contributing

Project layout

keyseal/
  cmd/keyseal/main.go         - entry point; calls cli.Execute()
  internal/
    buildinfo/                - version variables injected by ldflags
    cli/                      - Cobra command wiring; one file per command
    config/                   - keyseal.yaml load, validate, defaults
    doctor/                   - health check engine
    execenv/                  - subprocess runner + env merge
    fsutil/                   - atomic writes, mode checks, path validation
    gitutil/                  - Git subprocess helpers for repo-aware workflows
    render/                   - dotenv/JSON/YAML formatters, multi-doc merge
    repo/                     - logical name ↔ file path mapping, discovery
    schema/                   - secret document struct, parser, validator
    sopsconfig/               - .sops.yaml readiness inspection shared by doctor and updatekeys
    sopsutil/                 - SOPS library decrypt + subprocess wrappers (encrypt/edit/updatekeys/version)
    templates/                - built-in starter documents
  examples/
    keyseal.yaml              - reference config
    .sops.yaml                - reference SOPS config
    production-platform-app.plaintext.yaml
  .github/workflows/
    ci.yml                    - CI: fmt-check + test + build
    release.yml               - release: check + dist + GitHub release
  Makefile
  go.mod / go.sum

The internal/ packages are not exported. All functionality is consumed through the cli/ package, which wires Cobra commands to the internal packages.

Where commands live

Each command is a file in internal/cli/:

File Command
root.go Root command wiring, Execute(), --version flag
init.go keyseal init
add.go keyseal add
edit.go keyseal edit
status.go keyseal status
diff.go keyseal diff
history.go keyseal history
commit.go keyseal commit
updatekeys.go keyseal updatekeys
rollback.go keyseal rollback
render.go keyseal render
exec.go keyseal exec
doctor.go keyseal doctor
verify.go keyseal verify
version.go keyseal version

Adding a new command means: add a file in internal/cli/, register it in root.go's Execute() function, and put the business logic in an appropriate internal/ package.

Running tests

make test

This runs go test ./.... Tests are in _test.go files alongside their packages. There are no separate integration test directories - the approach in existing tests is to use real temp directories with stub SOPS binaries for subprocess-touching tests.

Git workflow tests follow the same pattern. internal/gitutil/gitutil_test.go and internal/cli/git_workflow_test.go create temporary Git repositories, configure a test author identity, and exercise real Git commands instead of mocking them.

The stub SOPS binary pattern is still used for mutating CLI tests. Read-only decrypt tests use real SOPS-encrypted fixtures and the official SOPS Go decrypt library so they prove render/exec/doctor do not shell out.

Formatting

make fmt         # format in place
make fmt-check   # check without modifying (used in CI)

CI enforces formatting. Any PR with unformatted Go files will fail the fmt-check step.

Before submitting changes

make check

This runs fmt-check, test, and build in sequence. It is the same sequence CI runs before a release. If make check passes locally, the CI gate should pass too.

Adding a command

  1. Create internal/cli/<command>.go with a newXxxCommand() *cobra.Command function
  2. Register it in root.go: add root.AddCommand(newXxxCommand())
  3. Put business logic in an appropriate internal/ package, not directly in the CLI file
  4. Add tests in internal/cli/<command>_test.go

Keep CLI files thin - they should parse arguments, validate flags, call internal packages, and format output. Avoid putting logic that needs to be tested in isolation directly in the command function.

For Git-aware behavior, keep raw Git subprocess calls inside internal/gitutil/. CLI code should resolve logical names, decide which paths are relevant, and format the result, but not shell out to git directly.

Adding a template

Templates live in internal/templates/templates.go. The templateMap variable maps template names to map[string]string key/value pairs.

Add an entry to templateMap and a case in the switch inside Build(). Add the new name to any documentation that lists available templates.

Write a test in internal/templates/templates_test.go confirming the new template produces the expected keys.

Changing the config schema

Config is in internal/config/config.go. The struct, defaults, and validation are all in the same file.

If you add a new field:

  1. Add it to the struct with a yaml: tag
  2. Set a default in Default()
  3. Apply the default in applyDefaults() (only if the field is zero-valued after unmarshal)
  4. Validate it in Validate() if there are constraints
  5. Add a test in internal/config/config_test.go

Changing the secret schema

Schema is in internal/schema/schema.go. The document struct, validation, parser, and marshaler are there.

The schema version is currently 1. If you introduce breaking changes to the schema, increment SupportedVersion and update validation to reject documents with the old version number.

Changing doctor checks

Doctor logic is split between internal/doctor/doctor.go (check functions) and internal/doctor/model.go (types and rendering). Shared SOPS config inspection is in internal/sopsconfig/, and external binary probing is in internal/toolcheck/.

Each check appends CheckResult values to the result via helper functions like appendOK, appendWarn, appendFail. The name field on a result is what appears in text and JSON output.

Tests for doctor checks are in internal/cli/doctor_test.go and internal/doctor/doctor_test.go. They use temp directories with known content and verify the check names, statuses, and summary text.

Dependencies

Keyseal depends on Cobra, YAML parsing, and the official SOPS Go module. Read-only decrypt uses the SOPS Go decrypt library; mutating encryption/edit/updatekeys workflows still use the external SOPS binary.

Run make tidy after changing go.mod.

License

Keyseal is licensed under GPL-3.0-only. Contributions are accepted under the same license.

Clone this wiki locally