Skip to content

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"]

Health Check Endpoint

func main() {
    sdk, _ := kiket.New(kiket.Config{...})

    // Webhook handler
    http.Handle("/webhook", sdk)

    // Health check
    http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
        w.Write([]byte("ok"))
    })

    log.Fatal(http.ListenAndServe(":8080", nil))
}