Skip to content

BlZvi/rest-city

Repository files navigation

rest-city

A modern, idiomatic Go SDK for the TeamCity REST API, designed for automation and Temporal workflows.

Features

  • Comprehensive API Coverage: Projects, Build Configurations, Builds, Queue, Artifacts, Health, and Server
  • Modern Go: Requires Go 1.22+, zero dependencies (stdlib only)
  • Flexible Authentication: Token-based and Basic Auth support
  • Context-Aware: All operations accept context.Context for cancellation and timeouts
  • Type-Safe: Strongly-typed models and interfaces
  • Production-Ready: Fully tested, well-documented, used in production environments

Installation

go get github.com/zvib/rest-city

Quick Start

package main

import (
    "context"
    "fmt"
    "log"
    "os"

    teamcity "github.com/zvib/rest-city"
)

func main() {
    // Create client with token authentication
    client := teamcity.NewClient(
        "https://teamcity.example.com",
        teamcity.WithToken(os.Getenv("TEAMCITY_TOKEN")),
    )

    ctx := context.Background()

    // List projects
    projects, err := client.Projects.List(ctx)
    if err != nil {
        log.Fatal(err)
    }

    for _, project := range projects {
        fmt.Printf("Project: %s (%s)\n", project.Name, project.ID)
    }
}

Table of Contents

Authentication

Token Authentication (Recommended)

client := teamcity.NewClient(
    "https://teamcity.example.com",
    teamcity.WithToken("your-token-here"),
)

How to create a token:

  1. Log in to TeamCity
  2. Click your username (top right) → "Access Tokens"
  3. Click "Create access token"
  4. Give it a name and set permissions
  5. Copy the token immediately (you won't see it again!)

Store tokens securely:

  • Use environment variables: os.Getenv("TEAMCITY_TOKEN")
  • Use secret managers (AWS Secrets Manager, HashiCorp Vault, etc.)
  • Never commit tokens to git

Basic Authentication

client := teamcity.NewClient(
    "https://teamcity.example.com",
    teamcity.WithBasicAuth("username", "password"),
)

Note: Token auth is more secure and recommended for production use.

Custom HTTP Client

httpClient := &http.Client{
    Timeout: 30 * time.Second,
    Transport: &http.Transport{
        TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // For self-signed certs
    },
}

client := teamcity.NewClient(
    "https://teamcity.example.com",
    teamcity.WithToken(token),
    teamcity.WithHTTPClient(httpClient),
    teamcity.WithTimeout(60*time.Second),
)

Usage Examples

Server

Get Server Information

// Get complete server information
info, err := client.Server.GetInfo(ctx)
if err != nil {
    log.Fatal(err)
}

fmt.Printf("TeamCity Version: %s\n", info.Version)
fmt.Printf("Build Number: %s\n", info.BuildNumber)
fmt.Printf("Start Time: %s\n", info.StartTime)

Get Specific Server Field

// Get just the version
version, err := client.Server.GetField(ctx, "version")
if err != nil {
    log.Fatal(err)
}
fmt.Printf("Version: %s\n", version)

Get Installed Plugins

// List all plugins
plugins, err := client.Server.GetPlugins(ctx)
if err != nil {
    log.Fatal(err)
}

fmt.Printf("Installed plugins: %d\n", plugins.Count)
for _, plugin := range plugins.Plugin {
    fmt.Printf("- %s (v%s)\n", plugin.DisplayName, plugin.Version)
}

Get Licensing Information

// Get licensing data
licensing, err := client.Server.GetLicensingData(ctx)
if err != nil {
    log.Fatal(err)
}

fmt.Printf("License Type: %s\n", licensing.ServerLicenseType)
fmt.Printf("Max Agents: %d\n", licensing.MaxAgents)
fmt.Printf("Unlimited Agents: %v\n", licensing.UnlimitedAgents)

if licensing.LicenseUseExceeded {
    log.Println("WARNING: License usage exceeded!")
}

Manage License Keys

// Get all license keys
keys, err := client.Server.GetLicenseKeys(ctx)
if err != nil {
    log.Fatal(err)
}

for _, key := range keys.LicenseKey {
    fmt.Printf("License: %s (Valid: %v, Expires: %s)\n",
        key.Type, key.Valid, key.ExpirationDate)
}

// Add a new license key
err = client.Server.AddLicenseKey(ctx, "your-license-key-here")
if err != nil {
    log.Fatal(err)
}

// Remove a license key
err = client.Server.DeleteLicenseKey(ctx, "old-license-key")
if err != nil {
    log.Fatal(err)
}

Get Server Metrics

// Get all server metrics
metrics, err := client.Server.GetMetrics(ctx)
if err != nil {
    log.Fatal(err)
}

for _, metric := range metrics.Metric {
    fmt.Printf("%s: %s\n", metric.Name, metric.Value)
}

Health

Check Server Accessibility

// Simple ping check
ok, err := client.Health.Ping(ctx)
if err != nil {
    log.Fatal(err)
}
fmt.Printf("Server accessible: %v\n", ok)

Get Server Information

// Get detailed server info
info, err := client.Health.GetServerInfo(ctx)
if err != nil {
    log.Fatal(err)
}

fmt.Printf("TeamCity Version: %s\n", info.Version)
fmt.Printf("Build Number: %s\n", info.BuildNumber)
fmt.Printf("Start Time: %s\n", info.StartTime)
fmt.Printf("Web URL: %s\n", info.WebURL)

Get All Health Items

// Get all health status items
items, err := client.Health.GetHealthItems(ctx)
if err != nil {
    log.Fatal(err)
}

fmt.Printf("Total health items: %d\n", items.Count)
for _, item := range items.Items {
    fmt.Printf("[%s] %s: %s\n", item.Severity, item.Identity, item.Description)
}

Get Health Categories

// Get all health categories
categories, err := client.Health.GetCategories(ctx)
if err != nil {
    log.Fatal(err)
}

for _, cat := range categories.Categories {
    fmt.Printf("Category: %s - %s\n", cat.Name, cat.Description)
}

Check for Critical Issues

// Check for errors and warnings
items, err := client.Health.GetHealthItems(ctx)
if err != nil {
    log.Fatal(err)
}

hasErrors := false
for _, item := range items.Items {
    if item.Severity == "ERROR" {
        fmt.Printf("ERROR: %s\n", item.Description)
        hasErrors = true
    }
}

if hasErrors {
    log.Fatal("TeamCity has critical health issues")
}

Projects

List All Projects

projects, err := client.Projects.List(ctx)
if err != nil {
    log.Fatal(err)
}

for _, p := range projects {
    fmt.Printf("%s: %s\n", p.ID, p.Name)
}

Get a Project

project, err := client.Projects.Get(ctx, "MyProject")
if err != nil {
    log.Fatal(err)
}
fmt.Printf("Project: %s (Parent: %s)\n", project.Name, project.ParentProjectID)

Create a Project

project, err := client.Projects.Create(ctx, teamcity.CreateProjectRequest{
    ID:          "MyNewProject",
    Name:        "My New Project",
    ParentID:    "ParentProject",
    Description: "This is a new project",
})
if err != nil {
    log.Fatal(err)
}

List Build Configurations in a Project

buildTypes, err := client.Projects.GetBuildTypes(ctx, "MyProject")
if err != nil {
    log.Fatal(err)
}

for _, bt := range buildTypes {
    fmt.Printf("Build Config: %s (ID: %s)\n", bt.Name, bt.ID)
}

Build Configurations

Get a Build Configuration

buildType, err := client.BuildTypes.Get(ctx, "MyProject_Build")
if err != nil {
    log.Fatal(err)
}
fmt.Printf("Build Type: %s (Paused: %v)\n", buildType.Name, buildType.Paused)

Create a Build Configuration

buildType, err := client.BuildTypes.Create(ctx, "MyProject", teamcity.CreateBuildTypeRequest{
    ID:   "MyProject_NewBuild",
    Name: "New Build Configuration",
})

Enable/Disable a Build Configuration

// Disable
err := client.BuildTypes.Disable(ctx, "MyProject_Build")

// Enable
err = client.BuildTypes.Enable(ctx, "MyProject_Build")

Builds

Trigger a Build

build, err := client.Builds.Trigger(ctx, teamcity.TriggerBuildRequest{
    BuildTypeID: "MyProject_Build",
    Branch:      "refs/heads/main",
    Parameters: map[string]string{
        "env.VERSION": "1.2.3",
        "env.DEPLOY":  "production",
    },
    Tags:    []string{"release", "v1.2.3"},
    Comment: "Triggered by automation",
})
if err != nil {
    log.Fatal(err)
}

fmt.Printf("Build #%d queued\n", build.ID)

Get a Single Build

// Get build by ID
build, err := client.Builds.Get(ctx, "12345")
if err != nil {
    log.Fatal(err)
}

fmt.Printf("Build #%s: %s (%s)\n", build.Number, build.Status, build.State)

Get All Builds

// Get the latest 50 builds across all projects
builds, nextHref, err := client.Builds.GetAll(ctx, 50)
if err != nil {
    log.Fatal(err)
}

for _, build := range builds {
    fmt.Printf("Build #%s: %s - %s\n", build.Number, build.BuildTypeID, build.Status)
}

// Use nextHref for pagination if needed

Wait for Build Completion

build, err := client.Builds.Trigger(ctx, teamcity.TriggerBuildRequest{
    BuildTypeID: "MyProject_Build",
})
if err != nil {
    log.Fatal(err)
}

// Poll until finished
buildID := strconv.FormatInt(build.ID, 10)
for {
    b, err := client.Builds.Get(ctx, buildID)
    if err != nil {
        log.Fatal(err)
    }

    if b.State == teamcity.StateFinished {
        fmt.Printf("Build finished: %s\n", b.Status)
        if b.Status == teamcity.StatusSuccess {
            fmt.Println("Build succeeded!")
        } else {
            fmt.Printf("Build failed: %s\n", b.StatusText)
        }
        break
    }

    fmt.Printf("Build state: %s\n", b.State)
    time.Sleep(5 * time.Second)
}

List Builds with Filters

builds, nextHref, err := client.Builds.List(ctx, teamcity.BuildsFilter{
    BuildTypeID: "MyProject_Build",
    Branch:      "refs/heads/main",
    Status:      teamcity.StatusSuccess,
    State:       teamcity.StateFinished,
    Count:       10,
})
if err != nil {
    log.Fatal(err)
}

for _, build := range builds {
    fmt.Printf("Build #%s: %s\n", build.Number, build.Status)
}

// Use nextHref for pagination

Pin/Unpin a Build

// Pin with comment
err := client.Builds.Pin(ctx, "12345", "Pinning for release")

// Unpin
err = client.Builds.Unpin(ctx, "12345")

Manage Build Tags

// Add tags
err := client.Builds.AddTags(ctx, "12345", []string{"release", "production", "v2.0"})

// Get tags
tags, err := client.Builds.GetTags(ctx, "12345")
for _, tag := range tags {
    fmt.Printf("Tag: %s\n", tag.Name)
}

// Remove a tag
err = client.Builds.RemoveTag(ctx, "12345", "release")

Cancel a Build

err := client.Builds.Cancel(ctx, "12345", "Canceling due to timeout", false)

Build Queue

List Queued Builds

queued, err := client.Queue.List(ctx)
if err != nil {
    log.Fatal(err)
}

for _, build := range queued {
    fmt.Printf("Queued: %s - %s\n", build.BuildTypeID, build.WaitReason)
}

Cancel a Queued Build

err := client.Queue.Cancel(ctx, "12345", "No longer needed")

Reorder Queue

// Move to top
err := client.Queue.MoveToTop(ctx, "12345")

// Move to bottom
err = client.Queue.MoveToBottom(ctx, "12345")

// Move to specific position
err = client.Queue.MoveToPosition(ctx, "12345", 3)

Artifacts

List Artifacts

artifacts, err := client.Artifacts.List(ctx, "12345", "")
if err != nil {
    log.Fatal(err)
}

for _, artifact := range artifacts {
    if artifact.IsDirectory() {
        fmt.Printf("[DIR]  %s\n", artifact.Name)
    } else {
        fmt.Printf("[FILE] %s (%d bytes)\n", artifact.Name, artifact.Size)
    }
}

Download an Artifact

err := client.Artifacts.DownloadToFile(
    ctx,
    "12345",
    "target/my-app.jar",
    "dist/my-app.jar",
)
if err != nil {
    log.Fatal(err)
}

Download All Artifacts as ZIP

err := client.Artifacts.DownloadAll(ctx, "12345", "artifacts.zip")

Stream Artifact Content

reader, err := client.Artifacts.Download(ctx, "12345", "logs/build.log")
if err != nil {
    log.Fatal(err)
}
defer reader.Close()

scanner := bufio.NewScanner(reader)
for scanner.Scan() {
    fmt.Println(scanner.Text())
}

Download from Latest Successful Build

err := client.Artifacts.DownloadLatest(
    ctx,
    "MyProject_Build",
    "target/app.jar",
    "latest-app.jar",
)

Common Use Cases

Monitor TeamCity Health Status

// Monitor for critical health issues before running builds
func checkTeamCityHealth(ctx context.Context, client *teamcity.Client) error {
    items, err := client.Health.GetHealthItems(ctx)
    if err != nil {
        return fmt.Errorf("failed to get health items: %w", err)
    }

    var errors []string
    var warnings []string

    for _, item := range items.Items {
        switch item.Severity {
        case "ERROR":
            errors = append(errors, fmt.Sprintf("%s: %s", item.Identity, item.Description))
        case "WARN":
            warnings = append(warnings, fmt.Sprintf("%s: %s", item.Identity, item.Description))
        }
    }

    if len(errors) > 0 {
        return fmt.Errorf("TeamCity has %d critical errors:\n%s", len(errors), strings.Join(errors, "\n"))
    }

    if len(warnings) > 0 {
        fmt.Printf("TeamCity has %d warnings (non-critical)\n", len(warnings))
    }

    fmt.Println("TeamCity health: OK")
    return nil
}

// Usage - check health before triggering builds
if err := checkTeamCityHealth(ctx, client); err != nil {
    log.Fatal(err)
}

// Proceed with build operations
build, err := client.Builds.Trigger(ctx, teamcity.TriggerBuildRequest{
    BuildTypeID: "MyProject_Build",
})

Connection Check with Retry

// Wait for TeamCity to be accessible (useful for startup/deployment)
func waitForTeamCity(ctx context.Context, client *teamcity.Client, maxRetries int) error {
    for i := 0; i < maxRetries; i++ {
        ok, err := client.Health.Ping(ctx)
        if err == nil && ok {
            fmt.Println("TeamCity is ready")
            return nil
        }

        fmt.Printf("Attempt %d/%d: TeamCity not ready, retrying...\n", i+1, maxRetries)
        time.Sleep(5 * time.Second)
    }
    return fmt.Errorf("TeamCity not accessible after %d retries", maxRetries)
}

Get Latest Build with a Specific Tag

// Find the most recent successful build with a specific tag
build, err := client.Builds.GetLatestByTag(ctx, "MyProject_Build", "production")
if err != nil {
    if teamcity.IsNotFound(err) {
        log.Println("No builds found with tag 'production'")
        return
    }
    log.Fatal(err)
}

fmt.Printf("Latest production build: #%s (ID: %d)\n", build.Number, build.ID)

Complete CI/CD Workflow

// 1. Trigger a build
build, err := client.Builds.Trigger(ctx, teamcity.TriggerBuildRequest{
    BuildTypeID: "MyProject_Build",
    Branch:      "refs/heads/main",
    Parameters: map[string]string{
        "env.VERSION": "1.2.3",
    },
    Tags:    []string{"release", "v1.2.3"},
    Comment: "Release build",
})
if err != nil {
    log.Fatal(err)
}

buildID := strconv.FormatInt(build.ID, 10)
fmt.Printf("Build #%d queued\n", build.ID)

// 2. Wait for completion
for {
    b, err := client.Builds.Get(ctx, buildID)
    if err != nil {
        log.Fatal(err)
    }

    if b.State == teamcity.StateFinished {
        if b.Status != teamcity.StatusSuccess {
            log.Fatalf("Build failed: %s", b.StatusText)
        }
        break
    }

    time.Sleep(5 * time.Second)
}

// 3. Pin the successful build
err = client.Builds.Pin(ctx, buildID, "Release v1.2.3")
if err != nil {
    log.Fatal(err)
}

// 4. Download artifacts
err = client.Artifacts.DownloadToFile(ctx, buildID, "target/my-app.jar", "dist/my-app.jar")
if err != nil {
    log.Fatal(err)
}

fmt.Println("Build and deployment completed successfully!")

Handle Build Errors Gracefully

build, err := client.Builds.Trigger(ctx, teamcity.TriggerBuildRequest{
    BuildTypeID: "MyProject_Build",
})
if err != nil {
    if teamcity.IsNotFound(err) {
        log.Println("Build configuration not found")
    } else if teamcity.IsUnauthorized(err) {
        log.Println("Invalid credentials")
    } else if teamcity.IsForbidden(err) {
        log.Println("Access denied - check permissions")
    } else {
        log.Printf("Error triggering build: %v", err)
    }
    return
}

// Monitor build and cancel if it takes too long
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute)
defer cancel()

ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()

for {
    select {
    case <-ctx.Done():
        // Timeout - cancel the build
        client.Builds.Cancel(ctx, strconv.FormatInt(build.ID, 10), "Build timeout", false)
        log.Fatal("Build timed out")
    case <-ticker.C:
        b, err := client.Builds.Get(ctx, strconv.FormatInt(build.ID, 10))
        if err != nil {
            log.Fatal(err)
        }

        if b.State == teamcity.StateFinished {
            if b.Status == teamcity.StatusSuccess {
                fmt.Println("Build succeeded!")
                return
            }
            log.Fatalf("Build failed: %s", b.StatusText)
        }
    }
}

Integration with Temporal Workflows

// Temporal activity for triggering builds
func TriggerTeamCityBuild(ctx context.Context, buildTypeID string, params map[string]string) (int64, error) {
    client := teamcity.NewClient(
        os.Getenv("TEAMCITY_URL"),
        teamcity.WithToken(os.Getenv("TEAMCITY_TOKEN")),
    )

    build, err := client.Builds.Trigger(ctx, teamcity.TriggerBuildRequest{
        BuildTypeID: buildTypeID,
        Parameters:  params,
    })
    if err != nil {
        return 0, err
    }

    return build.ID, nil
}

// Temporal activity for waiting on build completion
func WaitForBuildCompletion(ctx context.Context, buildID int64) (string, error) {
    client := teamcity.NewClient(
        os.Getenv("TEAMCITY_URL"),
        teamcity.WithToken(os.Getenv("TEAMCITY_TOKEN")),
    )

    ticker := time.NewTicker(10 * time.Second)
    defer ticker.Stop()

    for {
        select {
        case <-ctx.Done():
            return "", ctx.Err()
        case <-ticker.C:
            build, err := client.Builds.Get(ctx, strconv.FormatInt(buildID, 10))
            if err != nil {
                return "", err
            }

            if build.State == teamcity.StateFinished {
                return build.Status, nil
            }
        }
    }
}

Error Handling

The SDK provides typed errors for common HTTP status codes:

build, err := client.Builds.Get(ctx, "12345")
if err != nil {
    if teamcity.IsNotFound(err) {
        fmt.Println("Build not found")
    } else if teamcity.IsUnauthorized(err) {
        fmt.Println("Invalid credentials")
    } else if teamcity.IsForbidden(err) {
        fmt.Println("Access denied")
    } else {
        log.Fatal(err)
    }
}

Error Types

  • IsNotFound(err) - 404: Resource not found
  • IsUnauthorized(err) - 401: Invalid credentials
  • IsForbidden(err) - 403: Access denied
  • IsBadRequest(err) - 400: Invalid request
  • IsServerError(err) - 500: Server error

APIError Structure

type APIError struct {
    StatusCode int    // HTTP status code
    Body       []byte // Raw response body
    Message    string // Error message
}

Logging & Debugging

Quick Debug Mode

// Enable debug logging (easiest way)
client := teamcity.NewClient(
    "https://teamcity.example.com",
    teamcity.WithToken(token),
    teamcity.WithDebug(), // Logs all requests/responses
)

Output:

[TeamCity DEBUG] Request: GET https://teamcity.example.com/app/rest/projects
[TeamCity DEBUG] Response: 200
Body: {"count":3,"project":[...]}

Standard Library Logger

// Use standard library logger with custom prefix
client := teamcity.NewClient(
    "https://teamcity.example.com",
    teamcity.WithToken(token),
    teamcity.WithStdLogger("[TeamCity] "),
)

Custom Logger

// Implement your own logger
type customLogger struct {
    logger *zap.Logger
}

func (l *customLogger) Printf(format string, v ...interface{}) {
    l.logger.Info(fmt.Sprintf(format, v...))
}

client := teamcity.NewClient(
    "https://teamcity.example.com",
    teamcity.WithToken(token),
    teamcity.WithLogger(&customLogger{logger: zapLogger}),
)

FAQ

General

Q: What version of TeamCity does it support? A: TeamCity 2019.2+ (REST API v2). Works with both self-hosted and TeamCity Cloud.

Q: Is it production-ready? A: Yes! Fully tested, zero dependencies, used in production environments.

Q: Does it have dependencies? A: No external dependencies, stdlib only.

Authentication

Q: Should I use token auth or basic auth? A: Use token auth - it's more secure, can be scoped, and easier to rotate.

Q: Where should I store my token? A: Use environment variables or secret managers (AWS Secrets Manager, HashiCorp Vault). Never commit tokens to git.

Q: How do I handle token expiration? A: TeamCity tokens don't expire by default, but you can set expiration when creating them. Implement token refresh logic if needed.

Usage

Q: How do I poll for build status efficiently? A: Use 5-10 second intervals with context timeout:

ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute)
defer cancel()

ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()

for {
    select {
    case <-ctx.Done():
        return ctx.Err()
    case <-ticker.C:
        build, err := client.Builds.Get(ctx, buildID)
        // Check build state...
    }
}

Q: Can I use this with Temporal workflows? A: Yes! Perfect for Temporal activities. Pass context through, use reasonable polling intervals.

Q: How do I download large artifacts? A: Use streaming with Download() or direct file download with DownloadToFile():

// Streaming (memory efficient)
reader, err := client.Artifacts.Download(ctx, buildID, artifactPath)
defer reader.Close()
io.Copy(file, reader)

// Direct to file
err := client.Artifacts.DownloadToFile(ctx, buildID, artifactPath, localPath)

Q: How do I handle pagination? A: Use the nextHref returned by List() methods:

builds, nextHref, err := client.Builds.List(ctx, filter)
// Process builds...

if nextHref != "" {
    // Fetch next page using nextHref
}

Q: Can I pass custom HTTP headers? A: Yes, use WithHTTPClient() with a custom transport:

type headerTransport struct {
    base http.RoundTripper
}

func (t *headerTransport) RoundTrip(req *http.Request) (*http.Response, error) {
    req.Header.Set("X-Custom-Header", "value")
    return t.base.RoundTrip(req)
}

httpClient := &http.Client{
    Transport: &headerTransport{base: http.DefaultTransport},
}

client := teamcity.NewClient(url, teamcity.WithToken(token), teamcity.WithHTTPClient(httpClient))

Troubleshooting

401 Unauthorized

Problem: Getting 401 errors when making requests.

Solutions:

  • Verify token is correct: echo $TEAMCITY_TOKEN
  • Check token hasn't expired (TeamCity → Profile → Access Tokens)
  • Ensure token has necessary permissions
  • Try with basic auth to verify URL is correct
  • Enable debug logging to see exact requests

403 Forbidden

Problem: Getting 403 errors when trying to perform actions.

Solutions:

  • Check token permissions (needs appropriate scopes)
  • Verify user has permissions in TeamCity
  • Check if build configuration or project is restricted
  • Try the action manually in TeamCity UI

Connection Timeout

Problem: Requests timing out.

Solutions:

// Increase timeout
client := teamcity.NewClient(
    url,
    teamcity.WithToken(token),
    teamcity.WithTimeout(60*time.Second),
)

// Or use context timeout
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()

SSL Certificate Issues

Problem: SSL certificate verification failing.

Solutions:

// For development only - skip verification
httpClient := &http.Client{
    Transport: &http.Transport{
        TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
    },
}

client := teamcity.NewClient(url, teamcity.WithToken(token), teamcity.WithHTTPClient(httpClient))

// For production - use proper CA certs
certPool := x509.NewCertPool()
certPool.AppendCertsFromPEM(caCert)

httpClient := &http.Client{
    Transport: &http.Transport{
        TLSClientConfig: &tls.Config{RootCAs: certPool},
    },
}

Build Not Found

Problem: Getting 404 when fetching build.

Solutions:

  • Verify build ID is correct
  • Check if build has been deleted/cleaned up
  • Ensure you have permissions to view the build
  • Use build locator instead of ID: buildType:MyProject_Build

Artifact Download Fails

Problem: Artifact download failing or incomplete.

Solutions:

// Use streaming for large files
reader, err := client.Artifacts.Download(ctx, buildID, artifactPath)
if err != nil {
    log.Fatal(err)
}
defer reader.Close()

// Save with progress
file, _ := os.Create(localPath)
defer file.Close()

written, err := io.Copy(file, reader)
if err != nil {
    log.Fatal(err)
}
fmt.Printf("Downloaded %d bytes\n", written)

Rate Limiting

Problem: Getting rate limited by TeamCity.

Solutions:

  • Reduce polling frequency (use 10s instead of 1s)
  • Batch operations where possible
  • Use filters to reduce response sizes
  • Implement exponential backoff
  • Contact TeamCity admin to adjust rate limits

Memory Issues with Large Responses

Problem: High memory usage when listing many builds.

Solutions:

// Use smaller page sizes
builds, nextHref, err := client.Builds.List(ctx, teamcity.BuildsFilter{
    Count: 100, // Smaller batch
})

// Process incrementally
for nextHref != "" {
    // Process current batch
    // Fetch next batch
}

Best Practices

  1. Use Context - Always pass context for timeout and cancellation control
  2. Handle Errors - Check errors and use typed error helpers
  3. Poll Responsibly - Use reasonable intervals (5-10 seconds) when polling
  4. Use Constants - Use provided constants (StatusSuccess, StateFinished, etc.)
  5. Token Auth - Prefer token-based authentication over basic auth
  6. Reuse Clients - Create one client instance and reuse it
  7. Enable Logging - Use debug mode during development
  8. Stream Large Files - Use Download() for large artifacts
  9. Set Timeouts - Use context timeouts for long-running operations
  10. Graceful Degradation - Handle 404s gracefully (builds may be deleted)

Contributing

Contributions are welcome! Please:

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'feat: add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

See CONTRIBUTING.md for detailed guidelines.

License

MIT License - see LICENSE file for details.

Links

Support

For issues and questions, please use GitHub Issues.

About

No description, website, or topics provided.

Resources

License

Contributing

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 2

  •  
  •