Skip to content

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

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages