CLI Command Plugin
The most common plugin type. Adds a new terraci <command> subcommand that users can run from the terminal.
CI Pipeline Integration
A CommandProvider alone only adds a CLI command. To have your command run automatically as a step in generated CI pipelines, you must also implement PipelineContributor. This injects your command into the pipeline IR, and TerraCi generates the corresponding job/step in the output YAML.
Use Cases
- Slack/Teams notifications — post plan summaries to a channel
- Jira/Linear tickets — create issues from plan changes
- Audit reports — generate compliance reports from plan data
- Custom cost providers — extend cost estimation beyond AWS
- Deployment gates — check external approval systems before apply
Minimal Example
package slack
import (
"fmt"
"github.com/spf13/cobra"
"github.com/edelwud/terraci/pkg/plugin"
"github.com/edelwud/terraci/pkg/plugin/registry"
)
func init() {
registry.Register(&Plugin{
BasePlugin: plugin.BasePlugin[*Config]{
PluginName: "slack",
PluginDesc: "Post plan summaries to Slack",
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"`
WebhookURL string `yaml:"webhook_url"`
Channel string `yaml:"channel"`
}
// Commands implements plugin.CommandProvider.
func (p *Plugin) Commands(ctx *plugin.AppContext) []*cobra.Command {
var channel string
cmd := &cobra.Command{
Use: "slack",
Short: "Post plan summary to Slack",
RunE: func(cmd *cobra.Command, _ []string) error {
cfg := p.Config()
if channel == "" {
channel = cfg.Channel
}
fmt.Printf("Posting to %s via %s\n", channel, cfg.WebhookURL)
// your Slack API logic here
return nil
},
}
cmd.Flags().StringVar(&channel, "channel", "", "Slack channel (overrides config)")
return []*cobra.Command{cmd}
}Configuration
Users add your plugin to .terraci.yaml:
plugins:
slack:
enabled: true
webhook_url: "https://hooks.slack.com/services/T.../B.../xxx"
channel: "#terraform-deploys"Adding Flags
Use cobra's flag system. Flags are automatically shown in terraci slack --help:
func (p *Plugin) Commands(ctx *plugin.AppContext) []*cobra.Command {
var (
channel string
dryRun bool
format string
)
cmd := &cobra.Command{
Use: "slack",
Short: "Post plan summary to Slack",
RunE: func(cmd *cobra.Command, _ []string) error {
// use channel, dryRun, format
return nil
},
}
cmd.Flags().StringVar(&channel, "channel", "", "Slack channel")
cmd.Flags().BoolVar(&dryRun, "dry-run", false, "Preview without posting")
cmd.Flags().StringVar(&format, "output", "text", "Output format: text, json")
return []*cobra.Command{cmd}
}Multiple Subcommands
Return multiple commands to add a command group:
func (p *Plugin) Commands(ctx *plugin.AppContext) []*cobra.Command {
return []*cobra.Command{
{
Use: "notify send",
Short: "Send notification",
RunE: func(cmd *cobra.Command, _ []string) error { /* ... */ return nil },
},
{
Use: "notify status",
Short: "Check notification delivery",
RunE: func(cmd *cobra.Command, _ []string) error { /* ... */ return nil },
},
}
}Accessing Module Data
Use discovery.Scanner to find Terraform modules:
func runMyCommand(ctx context.Context, appCtx *plugin.AppContext) error {
cfg := appCtx.Config()
segments, err := config.ParsePattern(cfg.Structure.Pattern)
if err != nil {
return err
}
scanner := discovery.NewScanner(appCtx.WorkDir(), segments)
modules, err := scanner.Scan(ctx)
if err != nil {
return err
}
for _, m := range modules {
fmt.Printf("Module: %s (%d .tf files)\n", m.Path, len(m.Files))
}
return nil
}Reading Plan Results
For plugins that process terraform plan output:
func readPlanResults(appCtx *plugin.AppContext) error {
collection, err := discovery.ScanPlanResults(appCtx.ServiceDir())
if err != nil {
return err
}
for _, plan := range collection.Plans {
fmt.Printf("Module: %s, Changes: %d add, %d change, %d destroy\n",
plan.ModulePath, plan.Add, plan.Change, plan.Destroy)
}
return nil
}Heavy Initialization with RuntimeProvider
If your command needs expensive setup (API clients, caches), use the lazy runtime pattern:
type slackRuntime struct {
client *slack.Client
}
func (p *Plugin) Runtime(_ context.Context, _ *plugin.AppContext) (any, error) {
cfg := p.Config()
client := slack.New(cfg.WebhookURL)
return &slackRuntime{client: client}, nil
}
func (p *Plugin) runtime(ctx context.Context, appCtx *plugin.AppContext) (*slackRuntime, error) {
return plugin.BuildRuntime[*slackRuntime](ctx, p, appCtx)
}The runtime is only constructed when the command actually runs — not at startup.
Preflight Validation
Add cheap checks that run before any command:
func (p *Plugin) Preflight(_ context.Context, _ *plugin.AppContext) error {
cfg := p.Config()
if cfg.WebhookURL == "" {
return fmt.Errorf("slack: webhook_url is required")
}
return nil
}Adding to CI Pipelines
To run your command as a step in generated pipelines, implement PipelineContributor alongside CommandProvider:
import "github.com/edelwud/terraci/pkg/pipeline"
// PipelineContribution injects `terraci slack` into the PostPlan phase.
func (p *Plugin) PipelineContribution(_ *plugin.AppContext) *pipeline.Contribution {
cfg := p.Config()
if cfg == nil || !cfg.Pipeline {
return nil
}
return &pipeline.Contribution{
Jobs: []pipeline.ContributedJob{
{
Name: "slack-notify",
Phase: pipeline.PhasePostPlan,
DependsOnPlan: true,
Commands: []string{"terraci slack --channel " + cfg.Channel},
AllowFailure: true,
},
},
}
}Add a pipeline toggle to your config so users can opt in:
plugins:
slack:
enabled: true
channel: "#terraform-deploys"
pipeline: true # inject into CI pipelinetype Config struct {
Enabled bool `yaml:"enabled"`
Channel string `yaml:"channel"`
Pipeline bool `yaml:"pipeline"` // opt-in for pipeline integration
}This generates a standalone slack-notify job that runs after all plan jobs complete. Without PipelineContributor, users would have to manually add the step to their pipeline config.
Full Project Layout
terraci-plugin-slack/
├── go.mod
├── go.sum
├── plugin.go # init(), Plugin, Config
├── commands.go # CommandProvider
├── runtime.go # RuntimeProvider (optional)
├── lifecycle.go # Preflightable (optional)
└── README.mdBuild and Test
# Build with your plugin
xterraci build \
--with github.com/your-org/terraci-plugin-slack=./terraci-plugin-slack \
--output ./build/terraci
# Test
./build/terraci slack --channel #test --dry-runSee Also
- Pipeline Step Plugin — inject steps into CI pipelines
- Working Example