Help us shape the future of Go! Take the 2026 Go Developer Survey

releasebot

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 7, 2026 License: Apache-2.0 Imports: 22 Imported by: 0

README

release-bot

An autonomous release daemon in Go. It monitors one repository, drives a configurable coding agent over Agent Client Protocol (ACP), and verifies publication before recording a successful release. Codex through codex-acp is the default agent.

The Go code uses only the standard library. The acp package is a new implementation of the ACP v1 specification, with no third-party SDK or generated SDK code. This project has no HTTP server.

Run

Start it in the repository you want released:

cd /path/to/your-repo
release-bot

No configuration file is needed. The bot detects origin (or the only remote) and the repository's default branch, sets up its own checkout, and starts checking for releases. It works from subdirectories too. The agent reads the repository's instructions and discovers release workflows, destinations and publishability checks. Your existing working tree and uncommitted edits are left alone; releases use the remote branch's committed history.

If the repository has no release process, the agent is instructed to create it: build/package scripts, GitHub CI and release workflows, publishability checks, and RELEASING.md. It commits the setup, runs non-publishing validation, repairs failures, and then attempts the first release. Missing credentials or account permissions produce a specific blocked report before publication; missing workflows alone do not require manual setup.

You can also point it at a repository directly:

release-bot /path/to/repo
release-bot https://github.com/OWNER/REPO.git
release-bot --branch main --once
release-bot --agent your-acp-agent --agent-arg=--stdio
release-bot --model YOUR_MODEL_ID
release-bot status

--once performs one scheduled check or recovery attempt and exits. It can publish a release. --once --force skips cadence checks but still requires new commits. status shows progress without launching an agent. retry resets the pending release's attempt budget and resumes work; stop an already-running daemon before using it. Existing run and once subcommands also work. Flags can appear before or after the repository argument. Use --help for options.

The default workspace lives under $XDG_STATE_HOME/release-bot or ~/.local/state/release-bot, with separate checkout/state directories keyed by remote and branch. Restarting the same command reuses them. Both directories are locked against concurrent local processes, and the OS releases locks after crashes. Run only one bot per remote/branch across machines; there is no distributed lock.

Install and optional configuration

From this source checkout, run go install ./cmd/release-bot (Go 1.27.1), or make build to produce bin/release-bot. Runtime requirements are Linux/macOS, Git, Codex (or another authenticated ACP agent), and gh for GitHub repositories. Existing agent, Git and registry credentials are used. If codex-acp is installed, the bot uses it. Otherwise it automatically launches the maintained Codex ACP adapter through npx --yes @agentclientprotocol/codex-acp; Node.js must be installed and the first launch may download the adapter.

Advanced settings are optional: release-bot --config /path/to/release-bot.json uses an explicit configuration, with paths relative to that file. The example shows available settings. A config file is not auto-created or implicitly loaded. Use --json for machine-readable logs and status. Explicit agent commands are used exactly as configured.

Select a model with a flag:

release-bot --model YOUR_MODEL_ID

The flag works with Codex and other agents that advertise ACP model selection. The bot selects and confirms it before sending each preparation or publication prompt. Unknown model IDs report the agent's available choices; unsupported selection fails explicitly. Without the flag, the agent's default applies. An optional agent.model config value persists the choice; --model overrides it.

Cadence and recovery

By default the bot polls every five minutes and aims to release every 24 hours when there are unreleased commits. It may release earlier after 20 unreleased commits within two hours, provided two hours have elapsed since the last successful release and the branch has been quiet for 15 minutes. The daily deadline ignores the quiet period so continuous commits cannot starve releases. Set burst to zero to disable earlier releases.

Startup checks immediately: if the last release is at least daily old (24 hours by default), or there has never been a release, the bot starts release preparation on that first check. It does not wait for a polling interval, a quiet period, or another day after startup. Restarting does not reset the deadline. Existing releases still require unreleased commits, and every attempt must pass publishability checks before publishing.

On first startup, GitHub repositories use the most recently published release whose tag is reachable from the watched branch as their baseline. Existing release records are trusted for this initial baseline; an arbitrary local tag is not used. initial_ref explicitly overrides the baseline. Without an existing release or an explicit baseline, all commits are unreleased and the first check is immediately eligible. Non-GitHub repositories can set initial_ref to a known released commit/tag.

A release job records its target before starting the agent. Failed jobs preserve local work and failure evidence, then retry after 15 minutes. Each attempt has a two-hour budget shared by preparation, publishability validation, and publication. After three attempts, the job remains pending for operator inspection and retry. A stored publication receipt is rechecked before asking the agent to act again, including after the attempt budget is exhausted. This can reconcile a release whose asynchronous checks eventually pass.

Recovery instructions tell the agent to inspect existing tags, workflow runs and published artifacts before taking action. There is no atomic transaction spanning Git and external registries, so exactly-once publication cannot be guaranteed after a crash before the agent returns its receipt. Reconciliation reduces this risk. The baseline advances only to the verified tagged commit; later commits remain eligible for the next release.

Restarting immediately resumes a pending job, even if its saved retry timer has not elapsed. The failure budget still applies to actual failures. Ctrl+C or SIGTERM preserves unfinished work, records the stop reason, and does not consume an attempt or impose a retry delay. Ordinary polling after an actual failure retains the configured backoff.

Publishability before publication

Every attempt starts with a separate preparation session using the embedded preflight skill. That session can prepare code and validation infrastructure, but its instructions prohibit tags, public releases and registry uploads. It must enumerate every intended publication destination and return a plan containing:

  • The exact prepared commit and proposed tag.
  • Each destination's version and actual publishing identity/environment.
  • Reproducible non-publishing commands checking build/package validity, version availability and publishing authorization for each destination.
  • A post-publication verification command for each destination.

The daemon checks the plan's completeness, requires a clean checkout at the prepared commit, runs every preflight command itself, and checks the tree again. Only then does it start a separate publication session. A failed command or missing check prevents that session from starting. Publication must report the approved commit and tag. Each new attempt repeats preparation and validation; a fix that changes the plan requires new preflight checks.

For crates.io, cargo publish --dry-run checks packaging; it is not evidence of remote publish authorization. The skill requires separate checks of the actual publisher's ownership, token scope/expiry, or trusted-publisher configuration. When publishing through GitHub Actions, local credentials and secret names are insufficient: the checks must establish rights in the publishing workflow's environment. Unknown rights must block publication. The same principle applies to npm, PyPI, containers, signing and other destinations.

The agent discovers the repository-specific checks. Their correctness and destination coverage depend on the repository procedure and available registry capabilities; the daemon does not contain universal registry permission adapters. For an additional operator-maintained gate, configure preflight to a verifier outside the checkout. It runs after the destination checks, with RELEASE_COMMIT, RELEASE_TAG, RELEASE_TARGET and RELEASE_PLAN_JSON in the environment, and must exit nonzero if any destination is not demonstrably publishable.

"preflight": ["/opt/release-checks/publishability"]

After publishing, every destination's verification command must pass as well as the built-in GitHub checks and optional operator verifier. Missing registry artifacts keep the job pending even if its GitHub release exists. Preflight prevents foreseeable partial releases; network failures and non-transactional registries can still fail mid-publication. The publication skill directs the agent to stage privately where possible, publish the final release/announcement last, and reconcile partial artifacts on retry. The phase boundary is an agent instruction and daemon orchestration rule, not an OS/network sandbox.

GitHub is built in

The GitHub skill is embedded into every GitHub release prompt for every configured agent. It covers repository/workflow discovery, gh run list, failed job logs, reruns, dispatch, PR checks, publication and assets. The release skill instructs the agent to follow repository instructions, repair failures and finish the release.

The daemon also independently verifies with gh api:

  • The remote tag points to the reported full commit, includes the job target, and is reachable from the watched branch.
  • Actions runs belong to that exact tagged commit. The latest run/attempt in every observed workflow/event/ref context must finish with success.
  • Every required workflow has a run. The agent discovers workflow names/paths from the repository and records them in the publication plan; the daemon requires this list when none is configured. Optional github.workflows entries add operator requirements. Entries match workflow names, paths, or filenames.
  • A published, non-draft GitHub release exists for that tag. Uploaded assets must be nonempty and complete. Every github.assets glob must match at least one asset.

Pending or missing runs are polled within verification_timeout; failed, cancelled or skipped runs cause repair feedback. A passing branch run cannot mask a failed tag run. The verifier paginates API results and refuses incomplete histories. Destination-specific commands in the validated plan check package registries and artifact contents; an optional operator verify command can enforce additional requirements.

GitHub HTTPS/SSH remotes are detected automatically. For an enterprise host set github.host; for a local mirror set github.repo to OWNER/REPO. The gh process uses its own inherited authentication, independent of the agent's environment overrides.

"github": {
  "host": "github.com",
  "workflows": ["ci.yml", "release.yml"],
  "assets": ["*-linux-amd64.tar.gz", "checksums.txt"]
}

Other agents and release destinations

Set agent.command to an executable and argument array. agent.environment supplies additional environment variables, agent.auth_method optionally selects an advertised protocol-driven login method, and agent.mode optionally selects a session mode. Authenticate interactive agents before starting the daemon. ACP v1 stdio agents with their own tools or client filesystem/terminal tools are supported; draft ACP v2 is not supported.

"agent": {
  "command": ["your-acp-agent", "--stdio"],
  "environment": {"YOUR_AGENT_SETTING": "value"}
}

verify is an optional extra command for any Git host. The agent already supplies mandatory destination-specific verification commands in its plan. An operator verifier runs directly as an argument array, in the checkout, with RELEASE_TAG, RELEASE_COMMIT, RELEASE_TARGET, RELEASE_URL, RELEASE_REMOTE, and RELEASE_BRANCH in its environment. Exit zero only when the exact release's checks, artifacts and registry publication have succeeded. Keep the verifier outside the agent's writable checkout. There is no shell expansion unless you explicitly configure a shell.

"verify": ["/opt/release-checks/verify-publication"]

Execution and diagnostics

Starting the bot authorizes unattended code edits, command execution, commits, pushes and publication for the configured repository. ACP permission requests are approved automatically and recorded. Instructions require focused fixes and respect for branch protections. Client file operations use os.Root to prevent paths and symlinks escaping the checkout. Shell commands and the external agent run with the bot account's OS permissions: this is not a sandbox. Run it as a dedicated account or in a container with the credentials and tools required for that repository.

Readable progress goes to stderr; --json selects structured logs. Private JSONL session transcripts, including tool updates and agent diagnostics, live under state_directory/sessions. The workspace location is printed at startup. State writes use fsync and atomic replacement. Retain state across deployments. Configure log retention externally; transcripts may include repository contents or command output.

Development and protocol scope

The live transcript is shown by default: agent messages and thought updates, tool output, completion/failure status, and agent stderr appear as they arrive. Text fragments are joined into readable lines. --json keeps these as structured stream events. Session transcripts also include a session_end record with the phase, error, context cancellation cause (when available), and transport failure, so a future interruption can be diagnosed from disk.

This repository's release instructions describe its CI and release workflows. CI runs on pushes and pull requests; the release workflow builds Linux/macOS archives, checks publisher access, and verifies staged assets before publication.

make check

To test your real ACP adapter and model with a read-only fixture, without running a release:

RELEASE_BOT_LIVE_SMOKE=1 RELEASE_BOT_LIVE_MODEL=gpt-5.6-sol go test -run '^TestLiveACP$' -v .

The ACP implementation includes newline JSON-RPC framing, bidirectional requests, ordered notifications, request errors and cancellation, initialization/version checks, session creation, optional authentication/modes, prompt completion, permission decisions, file reads/writes, and the full terminal lifecycle. Generic Call/Notify methods allow extension methods. Optional session history, MCP configuration, elicitation and v2 are not advertised. Protocol references are recorded in docs/protocol.md.

Tests use in-memory protocol peers, real subprocess pipes, temporary Git remotes and simulated registry checks. They cover permission failures, incomplete plans, partial publication, recovery, concurrent commits, scheduling, locking, path confinement and cancellation. They do not exercise real Codex credentials or publish to live registries.

Licensed under Apache License 2.0. The license text was obtained unmodified from the Apache Software Foundation's license endpoint.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ResolveAgent

func ResolveAgent(cfg *Config, automatic bool) error

ResolveAgent uses an installed adapter when available and otherwise lets npx provision the default Codex adapter. Explicit commands are never replaced.

func Retry

func Retry(cfg Config) error

func Run

func Run(ctx context.Context, cfg Config, log *slog.Logger, once, force bool) error

Run watches one repository. Once executes one poll/recovery attempt; force ignores cadence for a new release but never publishes without new commits.

Types

type Agent

type Agent interface {
	Execute(context.Context, string) (Result, error)
}

type AgentConfig

type AgentConfig struct {
	Command     []string          `json:"command"`
	Environment map[string]string `json:"environment,omitempty"`
	AuthMethod  string            `json:"auth_method,omitempty"`
	Mode        string            `json:"mode,omitempty"`
	Model       string            `json:"model,omitempty"`
}

type Config

type Config struct {
	Remote              string       `json:"remote"`
	Branch              string       `json:"branch"`
	Directory           string       `json:"directory"`
	StateDirectory      string       `json:"state_directory"`
	InitialRef          string       `json:"initial_ref,omitempty"`
	InstructionFiles    []string     `json:"instruction_files"`
	Agent               AgentConfig  `json:"agent"`
	GitHub              GitHubConfig `json:"github"`
	Poll                Duration     `json:"poll"`
	Daily               Duration     `json:"daily"`
	MinimumGap          Duration     `json:"minimum_gap"`
	Quiet               Duration     `json:"quiet"`
	BurstWindow         Duration     `json:"burst_window"`
	Burst               int          `json:"burst"`
	Timeout             Duration     `json:"timeout"`
	VerificationTimeout Duration     `json:"verification_timeout"`
	RetryDelay          Duration     `json:"retry_delay"`
	Attempts            int          `json:"attempts"`
	Verify              []string     `json:"verify,omitempty"`
	Preflight           []string     `json:"preflight,omitempty"`
}

func DefaultConfig

func DefaultConfig() Config

func Discover

func Discover(ctx context.Context, target, branch string) (Config, error)

Discover configures a managed checkout from a working tree (including a subdirectory), a bare repository, or a Git URL. It does not edit the source.

func ReadConfig

func ReadConfig(filename string) (Config, error)

func (Config) GitHubRepo

func (c Config) GitHubRepo() string

func (Config) Validate

func (c Config) Validate() error

type Destination

type Destination struct {
	Name        string                `json:"name"`
	Kind        string                `json:"kind"`
	Version     string                `json:"version"`
	Environment string                `json:"environment"`
	Checks      []PublishabilityCheck `json:"checks"`
	Verify      []string              `json:"verify"`
}

type Duration

type Duration time.Duration

func (Duration) MarshalJSON

func (d Duration) MarshalJSON() ([]byte, error)

func (*Duration) UnmarshalJSON

func (d *Duration) UnmarshalJSON(raw []byte) error

type GitHubConfig

type GitHubConfig struct {
	Repo      string   `json:"repo,omitempty"`
	Host      string   `json:"host"`
	Workflows []string `json:"workflows"`
	Assets    []string `json:"assets"`
}

type Job

type Job struct {
	Target       string           `json:"target"`
	Started      time.Time        `json:"started"`
	Tries        int              `json:"tries"`
	RetryAt      time.Time        `json:"retry_at"`
	Failure      string           `json:"failure,omitempty"`
	Interruption string           `json:"interruption,omitempty"`
	Candidate    *Result          `json:"candidate,omitempty"`
	Phase        string           `json:"phase"`
	Plan         *PublicationPlan `json:"plan,omitempty"`
}

type PublicationPlan

type PublicationPlan struct {
	Commit          string        `json:"commit"`
	Tag             string        `json:"tag"`
	Destinations    []Destination `json:"destinations"`
	GitHubWorkflows []string      `json:"github_workflows,omitempty"`
}

type PublishabilityCheck

type PublishabilityCheck struct {
	Kind     string   `json:"kind"`
	Command  []string `json:"command"`
	Evidence string   `json:"evidence"`
}

type Result

type Result struct {
	Status string           `json:"status"`
	Tag    string           `json:"tag"`
	Commit string           `json:"commit"`
	URL    string           `json:"url,omitempty"`
	Detail string           `json:"detail,omitempty"`
	Plan   *PublicationPlan `json:"plan,omitempty"`
}

type State

type State struct {
	Format     int       `json:"format"`
	Remote     string    `json:"remote"`
	Branch     string    `json:"branch"`
	Directory  string    `json:"directory"`
	Released   string    `json:"released"`
	ReleasedAt time.Time `json:"released_at"`
	Observed   string    `json:"observed"`
	ChangedAt  time.Time `json:"changed_at"`
	Job        *Job      `json:"job,omitempty"`
	LastResult *Result   `json:"last_result,omitempty"`
}

func ReadState

func ReadState(cfg Config) (*State, error)

Directories

Path Synopsis
Package acp implements the ACP v1 stdio client protocol using only Go's standard library.
Package acp implements the ACP v1 stdio client protocol using only Go's standard library.
cmd
release-bot command
internal
osrun
Package osrun supplies bounded command output for the Unix daemon.
Package osrun supplies bounded command output for the Unix daemon.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL