Skip to content

Shunt Fork — Interface Abstraction Spec

Target: git.rbtr.dev/laputacloudco/shunt (our fork of rbtr/shunt) Purpose: Abstract forge and metrics dependencies so the engine can run in Cloudflare Workers (which has no net/http TCP transport, no filesystem, and no process spawning).

The engine’s core logic stays unchanged. We swap concrete types for interfaces that both the standard Go implementation and a Workers-compatible implementation can satisfy.


1. internal/forge/client.go — Add ForgeClient interface

Section titled “1. internal/forge/client.go — Add ForgeClient interface”

Add this interface after the Client type definition. The Client struct already implements all these methods, so no changes to the existing client.

// ForgeClient is the interface that *Client implements.
// Both the standard *Client and a Workers-compatible implementation satisfy this.
type ForgeClient interface {
ListOpenPRs(ctx context.Context, owner, repo, base string) ([]PullRequest, error)
GetPR(ctx context.Context, owner, repo string, index int) (PullRequest, error)
AutomergeScheduled(ctx context.Context, owner, repo string, index int) (bool, error)
LatestCommitStatus(ctx context.Context, owner, repo, sha, statusContext string) (CommitStatus, bool, error)
RunStatus(ctx context.Context, owner, repo, sha, branch string) (string, error)
RunTargetURL(ctx context.Context, owner, repo, sha, branch string) (string, error)
SetCommitStatus(ctx context.Context, owner, repo, sha, statusContext, state, desc, targetURL string) error
ScheduleAutomerge(ctx context.Context, owner, repo string, index int, style, headSHA string) (ScheduleAutomergeResult, error)
CancelAutomerge(ctx context.Context, owner, repo string, index int) (bool, error)
DeleteBranch(ctx context.Context, owner, repo, branch string) error
Comment(ctx context.Context, owner, repo string, index int, body string) error
UpsertComment(ctx context.Context, owner, repo string, index int, marker, botUser, body string) error
ReadFile(ctx context.Context, owner, repo, ref, path string) ([]byte, error)
}

No other changes needed to internal/forge/client.go. The existing *Client satisfies this interface by virtue of implementing all the methods.


2. internal/engine/engine.go — Swap *forge.Client for interface

Section titled “2. internal/engine/engine.go — Swap *forge.Client for interface”

Change the Config struct:

// Before:
type Config struct {
Forge *forge.Client // concrete type
...
}
// After:
type Config struct {
Forge forge.ForgeClient // interface
...
}

That’s the only change needed in the engine. All callers that create an engine.Engine need to pass a forge.ForgeClient instead of *forge.Client. Since *forge.Client implements ForgeClient, existing production code continues to work without changes.


3. internal/engine/engine.go — Make metrics optional

Section titled “3. internal/engine/engine.go — Make metrics optional”

Some engine methods call e.cfg.Metrics without checking for nil. Add nil guards:

// Before:
e.cfg.Metrics.Observe(...)
// After:
if e.cfg.Metrics != nil {
e.cfg.Metrics.Observe(...)
}

Search for all occurrences of e.cfg.Metrics. in engine.go and wrap them in nil checks. There are about 3-4 such call sites.


4. internal/metrics/metrics.go — Make HTTP handler nil-safe

Section titled “4. internal/metrics/metrics.go — Make HTTP handler nil-safe”

The Handler() method may panic or misbehave if no registry is configured. Make it return nil instead:

func (c *Collector) Handler() http.Handler {
if c.registry == nil {
return nil
}
// ... existing logic
}

Same for StatusHandler() and StatusPageHandler().


The Stager interface is already an interface. No changes needed.

6. No changes needed to internal/checkpoint/

Section titled “6. No changes needed to internal/checkpoint/”

The Store interface is already an interface. No changes needed.


File Change Lines affected
internal/forge/client.go Add ForgeClient interface ~20 new lines
internal/engine/engine.go *forge.Clientforge.ForgeClient in Config 1 line
internal/engine/engine.go Nil guards around e.cfg.Metrics.* calls ~5 lines
internal/metrics/metrics.go Nil-safe Handler(), StatusHandler(), StatusPageHandler() ~6 lines

Total: ~30 lines of changes, all additive. No behavior changes for existing shunt deployments.

gondolier imports the fork:

go get git.rbtr.dev/laputacloudco/shunt@latest

gondolier provides:

  • internal/forge/ — Workers-compatible ForgeClient implementation
  • internal/gitops/ — Workers-compatible Stager implementation
  • internal/io/ — I/O adapter that wires engine → Workers