Skip to content

Writing a Custom Plugin

semrel plugins are standalone executables — any language that can read environment variables, write JSON to stdout, and exit with a meaningful code can implement the plugin contract.

This guide walks through building a minimal provider plugin in Go.

Every semrel plugin communicates through two channels:

ChannelDirectionPurpose
Environment variables→ pluginRelease context + plugin config
stdout (JSON)plugin →Results (analyzers only)
stderrplugin →Logs, warnings, plugin_schema_version=N
Exit codeplugin →0 = success, non-zero = abort release

semrel sets the following variables before executing any plugin:

VariableDescription
SEMREL_VERSIONsemrel CLI version
SEMREL_TAG_NAMEFull tag name (v1.2.3)
SEMREL_CURRENT_VERSIONCurrent project version
SEMREL_NEXT_VERSIONCalculated next version
SEMREL_BUMPBump level: major, minor, patch, or none
SEMREL_BRANCHCurrent git branch
SEMREL_TAG_PREFIXConfigured tag prefix
SEMREL_CHANGELOGGenerated changelog content
SEMREL_DRY_RUNtrue when running with --dry-run

Plugin-specific args: from .semrel.yaml are exposed as SEMREL_PLUGIN_<KEY>=<value> (uppercase key).

Every plugin should emit its schema version on startup:

Terminal window
echo "plugin_schema_version=1" >&2

This tells semrel which version of the env-var contract the plugin expects, enabling future compatibility checks.


  1. Create your module

    Terminal window
    mkdir semrel-plugin-my-provider
    cd semrel-plugin-my-provider
    go mod init github.com/yourorg/semrel-plugin-my-provider
  2. Add dependencies

    Terminal window
    go get github.com/SemRels/semrel-plugin-sdk # optional: helpers for env-var reading
  3. Implement the plugin

    Create cmd/plugin/main.go:

    package main
    import (
    "fmt"
    "os"
    )
    func main() {
    if err := run(os.Environ(), os.Stdout, os.Stderr); err != nil {
    fmt.Fprintln(os.Stderr, "error:", err)
    os.Exit(1)
    }
    }
    func run(env []string, stdout, stderr *os.File) error {
    // Announce schema version.
    fmt.Fprintln(stderr, "plugin_schema_version=1")
    // Read context from environment.
    nextVersion := os.Getenv("SEMREL_NEXT_VERSION")
    dryRun := os.Getenv("SEMREL_DRY_RUN") == "true"
    token := os.Getenv("SEMREL_PLUGIN_TOKEN") // from args: token: ${{ secrets.MY_TOKEN }}
    if token == "" {
    return fmt.Errorf("SEMREL_PLUGIN_TOKEN is required")
    }
    if dryRun {
    fmt.Fprintf(stderr, "[dry-run] would publish release %s\n", nextVersion)
    return nil
    }
    // TODO: call your platform API here.
    fmt.Fprintf(stderr, "published release %s\n", nextVersion)
    return nil
    }
  4. Build the binary

    semrel looks for a binary called semrel-plugin-<name> in ~/.semrel/plugins/ or $PATH.

    Terminal window
    go build -o semrel-plugin-my-provider ./cmd/plugin
    mkdir -p ~/.semrel/plugins
    cp semrel-plugin-my-provider ~/.semrel/plugins/
  5. Wire it into .semrel.yaml

    plugins:
    - uses: my-provider # resolves to semrel-plugin-my-provider
    args:
    token: ${{ env.MY_TOKEN }}
  6. Test it

    Terminal window
    semrel release --dry-run

The simplest testing approach is to call run() directly with mock environment values:

package main_test
import (
"bytes"
"testing"
"github.com/stretchr/testify/require"
)
func TestRunDryRun(t *testing.T) {
env := map[string]string{
"SEMREL_NEXT_VERSION": "1.2.0",
"SEMREL_DRY_RUN": "true",
"SEMREL_PLUGIN_TOKEN": "test-token",
}
var stdout, stderr bytes.Buffer
err := run(env, &stdout, &stderr)
require.NoError(t, err)
require.Contains(t, stderr.String(), "plugin_schema_version=1")
require.Contains(t, stderr.String(), "[dry-run] would publish release 1.2.0")
}

semrel itself is the recommended tool:

Terminal window
semrel release

Submit your plugin for listing in the official semrel plugin registry:

Terminal window
gh api POST https://registry.semrel.io/api/v1/plugins/submit \
--field name=my-provider \
--field description="My custom provider plugin" \
--field repository=https://github.com/yourorg/semrel-plugin-my-provider \
--field category=provider \
--field license=MIT

Or visit registry.semrel.io and click Submit Plugin.

Create schema/v1.json in your repository to document the SEMREL_PLUGIN_* variables your plugin accepts:

{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://registry.semrel.io/schemas/plugins/my-provider/v1.json",
"title": "my-provider plugin schema",
"type": "object",
"properties": {
"SEMREL_PLUGIN_TOKEN": {
"type": "string",
"description": "API token for the target platform."
}
},
"required": ["SEMREL_PLUGIN_TOKEN"]
}

This schema will be served by the registry at /schemas/plugins/my-provider/v1.json and enables editor autocomplete for users’ .semrel.yaml files.


TypeOutputExit on failurePurpose
AnalyzerJSON to stdoutYesDetermines the next version from commits
GeneratorNothing (side-effects)YesGenerates release artefacts (changelog, etc.)
ProviderNothing (side-effects)YesPublishes the release to a platform
ConditionNothingYes (non-zero)Gate — aborts release if conditions not met
HookNothing (side-effects)OptionalLifecycle callbacks (pre/post release)
UpdaterNothing (side-effects)YesUpdates version strings in project files