Plugin System
TerraCi uses a compile-time plugin system inspired by the database/sql driver pattern in Go. Plugins register themselves via init() and blank imports at build time.
Built-in Plugins
| Plugin | Purpose | Config Required |
|---|---|---|
git | Changed-module detection via git diff | No |
gitlab | GitLab CI pipeline generation and MR comments | Yes (presence activates) |
github | GitHub Actions workflow generation and PR comments | Yes (presence activates) |
summary | MR/PR plan summary comments | No (enabled by default) |
cost | Cloud cost estimation (AWS) | Yes (providers.aws.enabled: true) |
policy | OPA policy checks | Yes (enabled: true) |
tfupdate | Terraform dependency resolver and lock synchronizer | Yes (enabled: true) |
Activation Policies
Each plugin has an activation policy that determines when it participates in the current run.
Always Active
The git plugin requires no configuration. It provides changed-module detection for --changed-only mode and is always available.
Activated by Config Presence
gitlab and github plugins activate when their config section exists under extensions:. Removing the section disables them:
extensions:
gitlab: # presence of this section activates the plugin
image: { name: hashicorp/terraform:1.6 }Active by Default
summary is active unless explicitly disabled. It posts plan summaries to MR/PR comments:
extensions:
summary:
enabled: false # opt outExplicitly Enabled
cost, policy, and tfupdate must be explicitly opted in:
extensions:
cost:
providers:
aws:
enabled: true
policy:
enabled: true
sources:
- type: path
path: policies
tfupdate:
enabled: true
policy:
bump: minorCI Provider Detection
TerraCi auto-detects the active CI provider at runtime:
TERRACI_PROVIDERenv var -- explicit override:bashTERRACI_PROVIDER=gitlab terraci generate -o pipeline.yml- Environment variables --
GITLAB_CI=trueselects GitLab,GITHUB_ACTIONS=trueselects GitHub - Single active provider -- if only one CI provider is active, it is used automatically
If multiple providers are configured and no environment is detected, TerraCi returns an error with instructions to set TERRACI_PROVIDER.
Plugin Capabilities
Plugins implement one or more capability interfaces. Registry lifecycle facades own discovery; plugin authors only implement the interface contracts:
| Capability | Purpose | Plugins |
|---|---|---|
CommandProvider | CLI subcommands (terraci cost, terraci local-exec, etc.) | cost, policy, summary, tfupdate, localexec |
PipelineContributor | Add standalone DAG jobs to pipeline IR | cost, policy, summary, tfupdate |
InitContributor | Form fields for terraci init wizard | gitlab, github, cost, policy, summary, tfupdate |
PipelineGeneratorFactory | Create provider-specific generators from immutable pipeline IR (NewGenerator(*pipeline.IR)) | gitlab, github |
CommentServiceFactory | Create MR/PR comment service | gitlab, github |
EnvDetector | Detect CI environment from env vars | gitlab, github |
CIInfoProvider | Provider name, pipeline ID, commit SHA | gitlab, github |
ChangeDetectionProvider | SDK capability that embeds plugin-agnostic workflow.ChangeDetector for VCS diffs | git |
Preflightable | Cheap startup validation before commands run | gitlab, github, git, cost, policy, tfupdate |
VersionProvider | Contribute version info to terraci version | policy |
KVCacheProvider | Named key/value cache backend resolution | inmemcache |
BlobStoreProvider | Named blob/object store backend (NewBlobStore(ctx, appCtx, opts)) | diskblob |
A single plugin can implement multiple capabilities. For example, cost implements CommandProvider (the terraci cost command), PipelineContributor (adds cost estimation step to pipeline), InitContributor (adds toggle to init wizard), and Preflightable (config validation); its estimator runtime is a plugin-local lazy builder.
Plugin Lifecycle
Every plugin goes through the same lifecycle:
1. Register -- init() registers the plugin via registry.RegisterFactory()
2. Configure -- framework decodes the matching extensions.<key> YAML section
3. Preflight -- cheap validation (env detection, config checks)
4. Bind -- runflow builds immutable Prepared/AppContext state
5. Execute -- commands lazily build plugin-local typed runtimes as neededPreflight runs for all enabled plugins before any command. It must be fast and side-effect-light -- no network calls, no heavy state. Heavy work (API clients, caches, estimators) belongs in plugin-local runtime builders, which run only when a command actually needs them.
Custom Plugins
Building with xterraci
xterraci produces a custom TerraCi binary with additional or fewer plugins:
# Add an external plugin
xterraci build --with github.com/your-org/terraci-plugin-slack
# Pin a specific version
xterraci build --with github.com/your-org/terraci-plugin-slack@v1.2.0
# Use a local plugin during development
xterraci build --with github.com/your-org/plugin=../my-plugin
# Remove built-in plugins you don't need
xterraci build --without cost --without policy
# Combine
xterraci build \
--with github.com/your-org/terraci-plugin-slack \
--without cost \
--output ./build/terraci-customHow It Works
xterraci build:
- Creates a temporary Go module
- Generates a
main.gowith blank imports of selected plugins - Runs
go getfor each external plugin - Runs
go buildto produce the binary
The resulting binary is identical to the standard terraci but with a different set of plugins compiled in.
List Built-in Plugins
xterraci list-pluginsWriting a Plugin
A minimal external plugin needs:
1. Registration -- init() function that calls registry.RegisterFactory():
package myplugin
import (
"github.com/edelwud/terraci/pkg/plugin"
"github.com/edelwud/terraci/pkg/plugin/registry"
)
func init() {
registry.RegisterFactory(func() plugin.Plugin {
return &Plugin{
BasePlugin: plugin.BasePlugin[*Config]{
PluginName: "myplugin",
PluginDesc: "My custom plugin",
EnableMode: plugin.EnabledExplicitly,
DefaultCfg: func() *Config { return &Config{} },
IsEnabledFn: func(cfg *Config) bool {
return cfg != nil && cfg.Enabled
},
},
}
})
}
type Plugin struct {
plugin.BasePlugin[*Config]
}
type Config struct {
Enabled bool `yaml:"enabled"`
APIKey string `yaml:"api_key"`
}
func (c *Config) Clone() *Config {
if c == nil {
return nil
}
out := *c
return &out
}2. Capabilities -- implement the interfaces you need:
// CommandProvider -- adds `terraci myplugin` command
func (p *Plugin) Commands() []*cobra.Command {
return []*cobra.Command{{
Use: "myplugin",
Short: "Run my custom plugin",
RunE: func(cmd *cobra.Command, _ []string) error {
// your logic here
return nil
},
}}
}3. Go module -- publish as a Go module with a go.mod that depends on github.com/edelwud/terraci.
Plugin File Convention
For larger plugins, follow the one-file-per-capability convention used by built-in plugins:
| File | Contents |
|---|---|
plugin.go | init(), Plugin struct, BasePlugin embedding |
lifecycle.go | Preflightable implementation |
commands.go | CommandProvider with cobra commands |
runtime.go | Plugin-local lazy heavy state builder |
usecases.go | Command orchestration over typed runtime |
pipeline.go | PipelineContributor jobs |
init_wizard.go | InitContributor form fields |
output.go | CLI rendering helpers |
report.go | CI report assembly |
Working Example
See examples/external-plugin for a complete working example that adds terraci hello.
Configuration Reference
| Plugin | Config page |
|---|---|
| GitLab CI | config/gitlab |
| GitHub Actions | config/github |
| Summary Comments | config/summary |
| Cost Estimation | config/cost |
| Policy Checks | config/policy |
| Dependency Updates | config/tfupdate |