> ## Documentation Index
> Fetch the complete documentation index at: https://sitectl.libops.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Development

> Local development workflow for sitectl and its plugins, including Go workspaces, linting, testing, and integration tests.

## Local install

For local development, install all binaries into a directory on your `$PATH` so core sitectl can discover and invoke the plugin binaries. From the core repo:

```bash theme={null}
make install
```

This builds and installs `sitectl`, then changes into `../sitectl-isle` and `../sitectl-drupal` and runs `make install` in each. The plugin `install` targets run `make work` before building so they use the local sitectl checkout.

If you're only working on a single plugin:

```bash theme={null}
cd ../sitectl-isle && make install
cd ../sitectl-drupal && make install
```

## Application plugin template

Use [`sitectl-app-tmpl` v1.0.0](https://github.com/libops/sitectl-app-tmpl/tree/6c1b09ad9b5decc0c9e9fe1aa9f199700a6f5f9c) when starting a new application plugin for a Compose-backed app that uses shared services such as Traefik, MariaDB, or Solr. The template includes the standard SDK entrypoint, Compose create and rollout metadata, project discovery, ingress wiring, an explicit dev-mode opt-out, an app `exec` helper, debug/validate/healthcheck runners, a warning-only verification scaffold, GitHub Actions workflows, GoReleaser config, Makefile targets, and a local `go.work` helper. Pin the scaffold release and exact tag commit used to create the repository; do not generate a release-bound plugin from a moving `main` checkout.

After creating a repo from the template, replace the app constants, module path, binary name, Compose template repository, project discovery rules, migration placeholder, warning-only verification runner, and app-specific commands. Core `sitectl compose` owns build, init, up, down, logs, status, and rollout behavior; core service commands own shared MariaDB, Solr, Traefik, Valkey, and Memcached operations. Keep local workspace wiring in `go.work`; do not add sibling-module `replace` directives to `go.mod`.

See [Application plugin template](/contributing/app-template) for the template checklist.

## Developing against a local sitectl checkout

Plugins import sitectl as a Go module dependency. When you need to test plugin changes against an unreleased version of sitectl, use Go workspaces instead of `replace` directives:

```bash theme={null}
make work
```

This runs `scripts/use-go-work.sh`, which creates a `go.work` file that points the plugin at `../sitectl`. The `go.work` file is gitignored. It is local only and does not affect CI or releases.

<Warning>
  Never add a `replace` directive to `go.mod` for local development. Use `make work` instead. Replace directives affect everyone who builds the module; `go.work` files are local.
</Warning>

## Current working directory context detection

sitectl can use the current working directory as the active context when it is inside a supported Compose project. Core sitectl finds the Compose project root, then asks installed plugins whether they claim that project. The plugin owns the discovery rules.

Plugins should register discovery during startup:

```go theme={null}
sdk.SetComposeProjectDiscovery(plugin.ComposeProjectDiscovery{
    RequiredServices:         []string{"drupal"},
    RequiredComposerPackages: []string{"drupal/islandora"},
    Reason:                   "drupal service with drupal/islandora in composer.json",
})
```

The SDK exposes this through the private `__sitectl-rpc` entrypoint using the `project.detect` method. Core sitectl uses that method during lightweight plugin discovery and creates a transient local context named `.` when the project has no matching saved context. This context is in memory only; discovery must not write to `~/.sitectl/config.yaml`.

Discovery is cached for the lifetime of the `sitectl` process by canonical project path and requested plugin. Repeated subcommands in one invocation should not repeatedly shell out to every plugin.

Plugin-specific commands must use the shared SDK context resolver (`sdk.GetContext`) so `--context .` works consistently. Commands that bypass the SDK need an explicit reason and tests for cwd detection.

## Linting and testing

```bash theme={null}
make lint test
```

This runs the same lint and test invocation used in GitHub Actions. Run it before pushing. Lint includes `gofmt` and `golangci-lint`. Tests run with `-race`.

To run a single test:

```bash theme={null}
go test -v -race ./cmd -run TestFcrepoComponent
```

## Integration tests (ISLE plugin)

The ISLE plugin has an end-to-end `create` test that exercises the full site creation flow:

```bash theme={null}
make integration-test FCREPO_STATE=off ISLE_FILE_SYSTEM_URI=public SITECTL_CONTEXT=isle-test
```

This is the same script the GitHub Actions integration workflow runs. It requires Docker and a functioning sitectl installation.

## Key command flags

### `sitectl create isle`

| Flag                | Default                          | Description                              |
| ------------------- | -------------------------------- | ---------------------------------------- |
| `--template-repo`   | `https://github.com/libops/isle` | LibOps ISLE template repository to clone |
| `--template-branch` | `v1.0.0`                         | Stable template ref to clone from        |

After create, configure git remotes with normal git commands. sitectl removes the template repository's `.git` history before initializing the new checkout, so rollout only pulls when the active branch has an upstream configured outside sitectl.

### `sitectl component describe`

`sitectl component describe` and `sitectl component reconcile` accept `--codebase-rootfs`. The shared RPC contract carries this as `codebase_rootfs` so the same field works for Drupal and non-Drupal plugins. ISLE first uses the active context's saved rootfs. Without one, it detects the supported legacy nested layout, `drupal`, or the repository root; a v1 `git-root` create stores `.`. Pass `--codebase-rootfs` only when an explicit override is needed.

Use `--codebase-rootfs` in examples and tests. Compatibility flags may exist in older plugins, but docs should show the canonical rootfs flag.

```bash theme={null}
sitectl component describe --path /path/to/project
```

### `sitectl converge` / `sitectl validate` / `sitectl verify`

These commands use `DisableFlagParsing: true` and forward unclaimed flags to the plugin's registered runner. The ISLE plugin's `ConvergeRunner` and `ValidateRunner` both accept `--codebase-rootfs` to resolve the Drupal web root correctly. The ISLE plugin's `VerifyRunner` accepts expected-state flags such as `--fcrepo`, `--blazegraph`, `--iiif`, `--iiif-topology`, and `--bot-mitigation`.

```bash theme={null}
sitectl converge --report
sitectl validate
sitectl verify --fcrepo off --iiif triplet --bot-mitigation on
```

### `sitectl create`

`create` also uses `DisableFlagParsing: true` and forwards every argument after it to
the selected plugin's create runner. As a result, global `sitectl` flags such as
`--context` and `--log-level` must be placed **before** the `create` subcommand, while
plugin create flags (`--path`, `--type`, `--checkout-source`, `--setup-only`, and so
on) go after the target. Placing `--context` after `create` forwards it to the plugin,
which fails with `unknown flag: --context`.

```bash theme={null}
sitectl --context integration-test create drupal/default --path ./drupal --type local
```

## Why `make install` matters for plugin chaining

Rebuilding `sitectl` locally without reinstalling the plugin binaries means core command dispatch will not see your current plugin builds. The full install chain keeps all three binaries aligned while you work across:

* core command routing in `sitectl`
* stack logic in `sitectl-isle`
* Drupal-specific extensions in `sitectl-drupal`

## Core and Plugin Release Order

Plugin executables embed the sitectl SDK version selected by their `go.mod`; installing a newer core binary does not update SDK behavior inside an already-built plugin. The reviewed v1 plugin set uses core `v1.0.0` and RPC schema 1. Treat a major core change as a compatibility boundary even when a package dependency written as a lower bound would accept it.

Use this order for any release that raises the minimum core SDK version:

1. Tag and publish core `sitectl`. Wait for its complete release workflow, native package publication, and Homebrew formula update to pass before releasing a plugin that requires it.
2. In each affected plugin, update `github.com/libops/sitectl` to the published stable version, run `go mod tidy`, ensure integration tests install that core version or newer, and run the full plugin tests without a local `go.work` override.
3. Release `sitectl-drupal` and wait for all publication jobs before releasing `sitectl-isle`; then release the other application plugins. ISLE v1 packages require `sitectl-drupal >= 1.0.0` as well as `sitectl >= 1.0.0`, so the publish and install dependency order is core, Drupal, then ISLE. Homebrew cannot express the same minimum versions reliably, making this ordering the compatibility gate there.
4. Update and release `sitectl-app-tmpl` after the core SDK it demonstrates is published. Its warning-only verification and migration placeholders are scaffold instructions, not finished behavior for a derived application plugin.
5. Update `sitectl-docs` last. Record each published v1-compatible version and exact tag commit in `scripts/snippet-dependencies.json`, regenerate the snippets from those commits, and merge only after the manifest checker proves every tag resolves to its recorded SHA. Keep an excluded incompatible plugin out of the active module and generated command reference until it has a compatible stable release.

Plugin GoReleaser configurations run `scripts/check-sitectl-core-version.sh` and refuse a release built with an older or prerelease SDK. The v1 Debian, RPM, and APK packages declare their format-specific `sitectl >= 1.0.0` dependency, but release planning must still respect major protocol boundaries. Renovate can propose later module upgrades after tags exist, but the initial core-first bump is an intentional release step.
