diff --git a/assets/og-image.png b/assets/og-image.png new file mode 100644 index 0000000..3e43318 Binary files /dev/null and b/assets/og-image.png differ diff --git a/assets/og-image.svg b/assets/og-image.svg new file mode 100644 index 0000000..4914806 --- /dev/null +++ b/assets/og-image.svg @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Ship automations + from + your terminal + + pull · diff · lint · push · MCP — one CLI for humans and agents + + + wk + + + + + diff --git a/cmd/wk/main.go b/cmd/wk/main.go index dad63c9..734675d 100644 --- a/cmd/wk/main.go +++ b/cmd/wk/main.go @@ -6,9 +6,16 @@ import ( "os/signal" "github.com/workato-devs/wk/internal/commands" + wkversion "github.com/workato-devs/wk/internal/version" ) -// Set by goreleaser ldflags. +// These default to "dev"/"none"/"unknown" and are overwritten at build time +// by the Go linker's -X flags. Two build paths set them: +// - Makefile: -X main.version=$(VERSION) ... (VERSION from `git describe`) +// - .goreleaser.yaml: -X main.version={{.Version}} ... (release builds in CI) +// The variable names must stay version/commit/date so those -X flags keep +// matching (the linker matches by exact symbol name, e.g. main.version). +// A plain `go run`/`go build` with no -X flags leaves the defaults in place. var ( version = "dev" commit = "none" @@ -19,6 +26,8 @@ func main() { ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) defer cancel() - commands.SetVersionInfo(version, commit, date) + // Record build-time version info so every package (CLI version command, + // API client User-Agent, MCP client) reads from a single source. + wkversion.Set(version, commit, date) os.Exit(commands.Execute(ctx)) } diff --git a/internal/api/http_client.go b/internal/api/http_client.go index 2beae8b..ac75980 100644 --- a/internal/api/http_client.go +++ b/internal/api/http_client.go @@ -9,10 +9,9 @@ import ( "net/http" "os" "time" -) -// userAgent is sent on every outbound Workato API request. -const userAgent = "wk/dev" + "github.com/workato-devs/wk/internal/version" +) // HTTPClient implements the Client interface using HTTP. type HTTPClient struct { @@ -175,7 +174,7 @@ func (c *HTTPClient) do(ctx context.Context, method, path string, body, result a } req.Header.Set("Authorization", "Bearer "+c.token) - req.Header.Set("User-Agent", userAgent) + req.Header.Set("User-Agent", version.UserAgent()) if body != nil { req.Header.Set("Content-Type", "application/json") } @@ -237,7 +236,7 @@ func (c *HTTPClient) doRaw(ctx context.Context, method, path string) ([]byte, er } req.Header.Set("Authorization", "Bearer "+c.token) - req.Header.Set("User-Agent", userAgent) + req.Header.Set("User-Agent", version.UserAgent()) if c.verbose { fmt.Fprintf(os.Stderr, "[debug] %s %s%s\n", method, c.baseURL, path) diff --git a/internal/api/packages.go b/internal/api/packages.go index e222279..c7c24a5 100644 --- a/internal/api/packages.go +++ b/internal/api/packages.go @@ -9,6 +9,8 @@ import ( "net/http" "os" "time" + + "github.com/workato-devs/wk/internal/version" ) type packageService struct { @@ -63,7 +65,7 @@ func (s *packageService) Import(ctx context.Context, folderID int, data []byte, } req.Header.Set("Authorization", "Bearer "+s.client.token) - req.Header.Set("User-Agent", userAgent) + req.Header.Set("User-Agent", version.UserAgent()) req.Header.Set("Content-Type", "application/octet-stream") if s.client.verbose { diff --git a/internal/commands/root.go b/internal/commands/root.go index a651612..0a0f174 100644 --- a/internal/commands/root.go +++ b/internal/commands/root.go @@ -29,20 +29,6 @@ func (e ExitCodeError) Error() string { return fmt.Sprintf("exit code %d", e.Code) } -// Version info set by main via SetVersionInfo. -var ( - versionStr = "dev" - commitStr = "none" - dateStr = "unknown" -) - -// SetVersionInfo is called from main.go to inject ldflags values. -func SetVersionInfo(version, commit, date string) { - versionStr = version - commitStr = commit - dateStr = date -} - // RunContext carries resolved dependencies into every command handler. // No global state — everything a command needs is here. type RunContext struct { diff --git a/internal/commands/version.go b/internal/commands/version.go index 11b602d..a74febc 100644 --- a/internal/commands/version.go +++ b/internal/commands/version.go @@ -4,6 +4,8 @@ import ( "fmt" "github.com/spf13/cobra" + + "github.com/workato-devs/wk/internal/version" ) func newVersionCmd() *cobra.Command { @@ -20,16 +22,16 @@ func newVersionCmd() *cobra.Command { } info := map[string]string{ - "version": versionStr, - "commit": commitStr, - "date": dateStr, + "version": version.Version(), + "commit": version.Commit(), + "date": version.Date(), } if flagJSON { return rctx.Formatter.Format(cmd.OutOrStdout(), info) } - fmt.Fprintf(cmd.OutOrStdout(), "wk %s\n", versionStr) + fmt.Fprintf(cmd.OutOrStdout(), "wk %s\n", version.Version()) return nil }, } diff --git a/internal/mcp/client.go b/internal/mcp/client.go index 1ed0e1e..24eb4c6 100644 --- a/internal/mcp/client.go +++ b/internal/mcp/client.go @@ -11,6 +11,7 @@ import ( "time" "github.com/workato-devs/wk/internal/api" + "github.com/workato-devs/wk/internal/version" ) // Client is an MCP protocol client using Streamable HTTP transport. @@ -57,7 +58,7 @@ func (c *Client) Initialize(ctx context.Context) (*api.MCPServerInfo, error) { "capabilities": map[string]any{}, "clientInfo": map[string]any{ "name": "wk", - "version": "dev", + "version": version.Version(), }, }, ID: 1, @@ -125,6 +126,7 @@ func (c *Client) send(ctx context.Context, rpcReq jsonRPCRequest) (*jsonRPCRespo req.Header.Set("Content-Type", "application/json") req.Header.Set("Accept", "text/event-stream, application/json") + req.Header.Set("User-Agent", version.UserAgent()) if c.sessionID != "" { req.Header.Set("Mcp-Session-Id", c.sessionID) } diff --git a/internal/version/version.go b/internal/version/version.go new file mode 100644 index 0000000..1acc19f --- /dev/null +++ b/internal/version/version.go @@ -0,0 +1,55 @@ +// Package version holds build-time version metadata injected via goreleaser +// ldflags (see .goreleaser.yaml and the Makefile) and derives values from it, +// such as the HTTP User-Agent sent on every API request for backend telemetry. +package version + +import ( + "fmt" + "strings" +) + +// These are populated once at startup by Set. The real values originate from +// the Go linker's -X flags, which are declared in the Makefile and +// .goreleaser.yaml, applied to main.version/commit/date, then handed here via +// Set. They default to dev values so `go run`/`go test` builds (no -X flags) +// still produce a sensible User-Agent. +var ( + version = "dev" + commit = "none" + date = "unknown" +) + +// Set records the build-time version info. Called once from main, before any +// HTTP client is constructed. +func Set(v, c, d string) { + // Normalize a leading "v". Git tags are "v1.0.6-beta"; `git describe` in + // the Makefile keeps the "v", while goreleaser's {{.Version}} strips it. + // Trimming here keeps the version — and the telemetry User-Agent — + // consistent regardless of which build path produced the binary. + version = strings.TrimPrefix(v, "v") + commit = c + date = d +} + +// Version returns the build version string (e.g. "1.2.3" or "dev"). +func Version() string { return version } + +// Commit returns the build commit hash. +func Commit() string { return commit } + +// Date returns the build date. +func Date() string { return date } + +// UserAgent returns the HTTP User-Agent header value, e.g. "wk-cli/1.2.3". +// The backend telemetry pipeline attributes requests to the CLI and its +// version via this header, mirroring the old workato-platform-cli's +// "workato-platform-cli/" convention. +// +// The token is "wk-cli", not the bare command name "wk": the nginx access-log +// field this lands in is analyzed/tokenized, so a 2-char token like "wk" +// shows up as a stray token in unrelated user-agents and collides. "wk-cli" +// filters cleanly as a phrase ("wk" then "cli" adjacent), and it matches the +// token the original beta builds already sent, keeping telemetry continuous. +func UserAgent() string { + return fmt.Sprintf("wk-cli/%s", version) +} diff --git a/internal/version/version_test.go b/internal/version/version_test.go new file mode 100644 index 0000000..a0e1978 --- /dev/null +++ b/internal/version/version_test.go @@ -0,0 +1,47 @@ +package version + +import "testing" + +func TestSetPopulatesAccessors(t *testing.T) { + Set("1.2.3", "abc1234", "2026-06-30") + + if got, want := Version(), "1.2.3"; got != want { + t.Errorf("Version() = %q, want %q", got, want) + } + if got, want := Commit(), "abc1234"; got != want { + t.Errorf("Commit() = %q, want %q", got, want) + } + if got, want := Date(), "2026-06-30"; got != want { + t.Errorf("Date() = %q, want %q", got, want) + } +} + +func TestUserAgentFormat(t *testing.T) { + Set("1.2.3", "c", "d") + + if got, want := UserAgent(), "wk-cli/1.2.3"; got != want { + t.Errorf("UserAgent() = %q, want %q", got, want) + } +} + +// A leading "v" from a git tag (e.g. via `git describe` in the Makefile) must +// be normalized so the telemetry token is consistent across build paths. +func TestSetStripsLeadingV(t *testing.T) { + Set("v1.0.6-beta", "c", "d") + + if got, want := Version(), "1.0.6-beta"; got != want { + t.Errorf("Version() = %q, want %q (leading v should be stripped)", got, want) + } + if got, want := UserAgent(), "wk-cli/1.0.6-beta"; got != want { + t.Errorf("UserAgent() = %q, want %q", got, want) + } +} + +// The dev fallback must not be mangled by the "v" trimming. +func TestSetDevFallbackUnchanged(t *testing.T) { + Set("dev", "none", "unknown") + + if got, want := UserAgent(), "wk-cli/dev"; got != want { + t.Errorf("UserAgent() = %q, want %q", got, want) + } +}