package azure import ( "encoding/base64" "fmt" "net/http" "strings" "time" "github.com/go-resty/resty/v2" ) type Pipeline struct { ID int `json:"id"` Name string `json:"name"` Folder string `json:"folder"` URL string `json:"url"` } type BuildDefinition struct { ID int `json:"id"` Name string `json:"name"` Process struct { Type int `json:"type"` YamlFilename string `json:"yamlFilename"` // for YAML pipelines } `json:"process"` Variables map[string]struct { Value string `json:"value"` IsSecret bool `json:"isSecret"` } `json:"variables"` Repository struct { URL string `json:"url"` Name string `json:"name"` } `json:"repository"` } type Client struct { client *resty.Client organization string } type Repository struct { ID string `json:"id"` Name string `json:"name"` Project struct { Name string `json:"name"` } `json:"project"` RemoteURL string `json:"remoteUrl"` SSHURL string `json:"sshUrl"` WebURL string `json:"webUrl"` } type RepositoriesResponse struct { Count int `json:"count"` Value []Repository `json:"value"` } func NewClient(org, pat string) *Client { c := resty.New() c.SetTimeout(30 * time.Second) // Azure DevOps uses Basic auth with empty username and PAT as password auth := base64.StdEncoding.EncodeToString([]byte(":" + pat)) c.SetHeader("Authorization", "Basic "+auth) c.SetHeader("Content-Type", "application/json") return &Client{ client: c, organization: org, } } func (c *Client) GetRepositories() ([]Repository, error) { url := fmt.Sprintf("https://dev.azure.com/%s/_apis/git/repositories", c.organization) var result RepositoriesResponse resp, err := c.client.R(). SetQueryParam("api-version", "7.0"). SetResult(&result). Get(url) if err != nil { return nil, fmt.Errorf("failed to fetch repositories: %w", err) } if resp.StatusCode() != http.StatusOK { return nil, fmt.Errorf("Azure API returned status %d: %s", resp.StatusCode(), resp.String()) } // Handle pagination if needed (continuationToken) // For simplicity, this example handles single page if result.Count != len(result.Value) { fmt.Printf("Warning: Repository count mismatch, expected %d got %d\n", result.Count, len(result.Value)) } return result.Value, nil } func (c *Client) GetAuthenticatedURL(remoteURL, pat string) string { // Convert https://dev.azure.com/org/project/_git/repo // to https://pat@dev.azure.com/org/project/_git/repo if len(remoteURL) > 8 && remoteURL[:8] == "https://" { return "https://:" + pat + "@" + remoteURL[8:] } return remoteURL } // GetBuildDefinitions gets pipeline definitions for a project func (c *Client) GetBuildDefinitions(project string) ([]BuildDefinition, error) { url := fmt.Sprintf("https://dev.azure.com/%s/%s/_apis/build/definitions", c.organization, project) var result struct { Count int `json:"count"` Value []BuildDefinition `json:"value"` } resp, err := c.client.R(). SetQueryParam("api-version", "7.0"). SetQueryParam("includeAll", "true"). SetResult(&result). Get(url) if err != nil { return nil, fmt.Errorf("failed to fetch build definitions: %w", err) } if resp.StatusCode() != 200 { return nil, fmt.Errorf("Azure API returned %d: %s", resp.StatusCode(), resp.String()) } return result.Value, nil } // GetPipelineContent fetches the azure-pipelines.yml content from repository func (c *Client) GetPipelineContent(repoURL, branch, filePath string) (string, error) { // Azure DevOps Repos API for file content // Extract project and repo name from URL // URL format: https://dev.azure.com/org/project/_git/repo // For this example, construct the items API URL parts := strings.Split(repoURL, "/_git/") if len(parts) != 2 { return "", fmt.Errorf("cannot parse repo URL: %s", repoURL) } base := parts[0] repoName := parts[1] url := fmt.Sprintf("%s/_apis/sourceProviders/TfsGit/filecontents?repository=%s&commitOrBranch=%s&path=%s&api-version=7.0", base, repoName, branch, filePath) resp, err := c.client.R(). Get(url) if err != nil { return "", err } if resp.StatusCode() != 200 { // Try alternative: get via Git API (Items endpoint) return "", fmt.Errorf("file not found or access denied") } return string(resp.Body()), nil } // GetPipelineVariables gets variables for a specific build definition func (c *Client) GetPipelineVariables(project string, definitionID int) (map[string]string, error) { url := fmt.Sprintf("https://dev.azure.com/%s/%s/_apis/build/definitions/%d/variables", c.organization, project, definitionID) var result struct { Count int `json:"count"` Value map[string]struct { Value string `json:"value"` IsSecret bool `json:"isSecret"` } `json:"value"` } resp, err := c.client.R(). SetQueryParam("api-version", "7.0"). SetResult(&result). Get(url) if err != nil { return nil, err } if resp.StatusCode() != 200 { return nil, fmt.Errorf("failed to get variables: %d", resp.StatusCode()) } vars := make(map[string]string) for k, v := range result.Value { vars[k] = v.Value // Note: secrets won't have values via API unless explicitly allowed } return vars, nil }