A modern, idiomatic Go SDK for the TeamCity REST API, designed for automation and Temporal workflows.
- 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.Contextfor cancellation and timeouts - Type-Safe: Strongly-typed models and interfaces
- Production-Ready: Fully tested, well-documented, used in production environments
go get github.com/zvib/rest-citypackage 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)
}
}- Authentication
- Usage Examples
- Common Use Cases
- Error Handling
- Logging & Debugging
- FAQ
- Troubleshooting
- Contributing
client := teamcity.NewClient(
"https://teamcity.example.com",
teamcity.WithToken("your-token-here"),
)How to create a token:
- Log in to TeamCity
- Click your username (top right) → "Access Tokens"
- Click "Create access token"
- Give it a name and set permissions
- 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
client := teamcity.NewClient(
"https://teamcity.example.com",
teamcity.WithBasicAuth("username", "password"),
)Note: Token auth is more secure and recommended for production use.
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),
)// 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 just the version
version, err := client.Server.GetField(ctx, "version")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Version: %s\n", version)// 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 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!")
}// 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 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)
}// Simple ping check
ok, err := client.Health.Ping(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Server accessible: %v\n", ok)// 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 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 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 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, err := client.Projects.List(ctx)
if err != nil {
log.Fatal(err)
}
for _, p := range projects {
fmt.Printf("%s: %s\n", p.ID, p.Name)
}project, err := client.Projects.Get(ctx, "MyProject")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Project: %s (Parent: %s)\n", project.Name, project.ParentProjectID)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)
}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)
}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)buildType, err := client.BuildTypes.Create(ctx, "MyProject", teamcity.CreateBuildTypeRequest{
ID: "MyProject_NewBuild",
Name: "New Build Configuration",
})// Disable
err := client.BuildTypes.Disable(ctx, "MyProject_Build")
// Enable
err = client.BuildTypes.Enable(ctx, "MyProject_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 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 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 neededbuild, 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)
}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 with comment
err := client.Builds.Pin(ctx, "12345", "Pinning for release")
// Unpin
err = client.Builds.Unpin(ctx, "12345")// 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")err := client.Builds.Cancel(ctx, "12345", "Canceling due to timeout", false)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)
}err := client.Queue.Cancel(ctx, "12345", "No longer needed")// 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, 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)
}
}err := client.Artifacts.DownloadToFile(
ctx,
"12345",
"target/my-app.jar",
"dist/my-app.jar",
)
if err != nil {
log.Fatal(err)
}err := client.Artifacts.DownloadAll(ctx, "12345", "artifacts.zip")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())
}err := client.Artifacts.DownloadLatest(
ctx,
"MyProject_Build",
"target/app.jar",
"latest-app.jar",
)// 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",
})// 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)
}// 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)// 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!")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)
}
}
}// 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
}
}
}
}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)
}
}IsNotFound(err)- 404: Resource not foundIsUnauthorized(err)- 401: Invalid credentialsIsForbidden(err)- 403: Access deniedIsBadRequest(err)- 400: Invalid requestIsServerError(err)- 500: Server error
type APIError struct {
StatusCode int // HTTP status code
Body []byte // Raw response body
Message string // Error message
}// 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":[...]}
// Use standard library logger with custom prefix
client := teamcity.NewClient(
"https://teamcity.example.com",
teamcity.WithToken(token),
teamcity.WithStdLogger("[TeamCity] "),
)// 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}),
)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.
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.
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))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
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
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()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},
},
}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
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)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
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
}- Use Context - Always pass context for timeout and cancellation control
- Handle Errors - Check errors and use typed error helpers
- Poll Responsibly - Use reasonable intervals (5-10 seconds) when polling
- Use Constants - Use provided constants (
StatusSuccess,StateFinished, etc.) - Token Auth - Prefer token-based authentication over basic auth
- Reuse Clients - Create one client instance and reuse it
- Enable Logging - Use debug mode during development
- Stream Large Files - Use
Download()for large artifacts - Set Timeouts - Use context timeouts for long-running operations
- Graceful Degradation - Handle 404s gracefully (builds may be deleted)
Contributions are welcome! Please:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'feat: add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
See CONTRIBUTING.md for detailed guidelines.
MIT License - see LICENSE file for details.
For issues and questions, please use GitHub Issues.