Go SDK Cookbook¶
Common patterns and recipes for building Kiket extensions with Go.
Issue Processing¶
Auto-assign Issues¶
sdk.On("issue.created", func(ctx context.Context, payload kiket.WebhookPayload, hctx *kiket.HandlerContext) (interface{}, error) {
issue := payload["issue"].(map[string]interface{})
projectID := payload["project_id"]
// Get team members
resp, err := hctx.Client.Get(ctx, "/api/v1/ext/project/members", nil)
if err != nil {
return nil, err
}
var members struct {
Members []struct {
ID int `json:"id"`
Name string `json:"name"`
} `json:"members"`
}
json.Unmarshal(resp, &members)
// Assign to first available member
if len(members.Members) > 0 {
_, err = hctx.Client.Patch(ctx, fmt.Sprintf("/api/v1/ext/issues/%v", issue["id"]), map[string]interface{}{
"assignee_id": members.Members[0].ID,
}, nil)
}
return nil, err
})
Track Time on Status Change¶
sdk.On("issue.status_changed", func(ctx context.Context, payload kiket.WebhookPayload, hctx *kiket.HandlerContext) (interface{}, error) {
issue := payload["issue"].(map[string]interface{})
oldStatus := payload["old_status"].(string)
newStatus := payload["new_status"].(string)
projectID := payload["project_id"]
customData := hctx.Endpoints.CustomData(projectID)
// Record status transition time
_, err := customData.Create(ctx, "time-tracking", "transitions", map[string]interface{}{
"issue_id": issue["id"],
"from": oldStatus,
"to": newStatus,
"timestamp": time.Now().UTC().Format(time.RFC3339),
})
return nil, err
})
Custom Data Patterns¶
Sync External Data¶
func syncExternalRecords(ctx context.Context, hctx *kiket.HandlerContext, projectID interface{}) error {
customData := hctx.Endpoints.CustomData(projectID)
// Fetch from external API
apiToken, _ := hctx.Secrets.Get(ctx, "external_api_token")
// ... fetch external data ...
externalRecords := []map[string]interface{}{
{"external_id": "ext-1", "name": "Record 1"},
{"external_id": "ext-2", "name": "Record 2"},
}
for _, record := range externalRecords {
// Check if exists
existing, err := customData.List(ctx, "sync-module", "records", &kiket.CustomDataListOptions{
Filters: map[string]interface{}{
"external_id": record["external_id"],
},
})
if err != nil {
return err
}
if len(existing.Data) > 0 {
// Update
_, err = customData.Update(ctx, "sync-module", "records", existing.Data[0]["id"], record)
} else {
// Create
_, err = customData.Create(ctx, "sync-module", "records", record)
}
if err != nil {
return err
}
}
return nil
}
SLA Monitoring¶
Alert on SLA Breach¶
sdk.On("workflow.sla_status", func(ctx context.Context, payload kiket.WebhookPayload, hctx *kiket.HandlerContext) (interface{}, error) {
event := payload["sla_event"].(map[string]interface{})
state := event["state"].(string)
if state == "breached" {
issueID := event["issue_id"]
// Get webhook URL from secrets
webhookURL, _ := hctx.Secrets.Get(ctx, "slack_webhook_url")
if webhookURL == "" {
return nil, nil
}
// Send Slack notification
slackPayload := map[string]interface{}{
"text": fmt.Sprintf("SLA Breached for issue #%v", issueID),
}
body, _ := json.Marshal(slackPayload)
http.Post(webhookURL, "application/json", bytes.NewReader(body))
}
return nil, nil
})
Error Handling¶
Graceful Error Recovery¶
sdk.On("issue.created", func(ctx context.Context, payload kiket.WebhookPayload, hctx *kiket.HandlerContext) (interface{}, error) {
issue := payload["issue"].(map[string]interface{})
// Try primary action
err := primaryAction(ctx, hctx, issue)
if err != nil {
// Log error but don't fail
log.Printf("Primary action failed: %v", err)
// Try fallback
if fallbackErr := fallbackAction(ctx, hctx, issue); fallbackErr != nil {
// Both failed - return error
return nil, fmt.Errorf("primary: %v, fallback: %v", err, fallbackErr)
}
}
return map[string]string{"status": "ok"}, nil
})
Rate Limit Handling¶
func withRateLimitRetry(ctx context.Context, hctx *kiket.HandlerContext, fn func() error) error {
for retries := 0; retries < 3; retries++ {
err := fn()
if err == nil {
return nil
}
// Check if rate limited
var apiErr *kiket.APIError
if errors.As(err, &apiErr) && apiErr.StatusCode == 429 {
// Get rate limit info
info, _ := hctx.Endpoints.RateLimit(ctx)
if info != nil && info.ResetIn > 0 {
time.Sleep(time.Duration(info.ResetIn) * time.Second)
continue
}
}
return err
}
return fmt.Errorf("max retries exceeded")
}
Testing¶
Mock SDK for Unit Tests¶
type mockClient struct {
responses map[string][]byte
}
func (m *mockClient) Get(ctx context.Context, path string, opts *kiket.RequestOptions) ([]byte, error) {
if resp, ok := m.responses[path]; ok {
return resp, nil
}
return nil, &kiket.APIError{StatusCode: 404}
}
// Implement other methods...
func TestHandler(t *testing.T) {
mock := &mockClient{
responses: map[string][]byte{
"/api/v1/ext/project/members": []byte(`{"members":[]}`),
},
}
hctx := &kiket.HandlerContext{
Client: mock,
// ...
}
payload := kiket.WebhookPayload{
"issue": map[string]interface{}{"id": 1, "title": "Test"},
}
result, err := myHandler(context.Background(), payload, hctx)
// Assert...
}
Integration Test with Real Signatures¶
func TestWebhookSignature(t *testing.T) {
secret := "test-secret"
body := `{"event":"issue.created","issue":{"id":1}}`
sig, ts := kiket.GenerateSignature(secret, body, nil)
headers := kiket.Headers{
"X-Kiket-Signature": sig,
"X-Kiket-Timestamp": ts,
"X-Kiket-Event-Version": "v1",
}
err := kiket.VerifySignature(secret, []byte(body), headers)
if err != nil {
t.Fatalf("Signature verification failed: %v", err)
}
}
Deployment¶
Docker Container¶
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o extension .
FROM alpine:3.18
RUN apk --no-cache add ca-certificates
WORKDIR /app
COPY --from=builder /app/extension .
COPY extension.yaml .
EXPOSE 8080
CMD ["./extension"]