Partager via


Créer un bogue dans Azure DevOps Services à l’aide de bibliothèques clientes .NET

Azure DevOps Services | Azure DevOps Server | Azure DevOps Server 2022

La création d’éléments de travail par programmation est un scénario d’automatisation courant dans Azure DevOps Services. Cet article explique comment créer un bogue (ou tout élément de travail) à l’aide de bibliothèques clientes .NET avec des méthodes d’authentification modernes.

Conditions préalables

Catégorie Spécifications
Azure DevOps - Une organisation
- Accès à un projet dans lequel vous pouvez créer des éléments de travail
Authentification Choisissez une des options suivantes :
- Authentification Microsoft Entra ID (recommandé)
- Jeton d’accès personnel (PAT) ( pour les tests)
environnement de développement Environnement de développement C#. Vous pouvez utiliser Visual Studio

Importante

Pour les applications de production, nous vous recommandons d’utiliser l’authentification Microsoft Entra ID au lieu de jetons d’accès personnels. Les PAT conviennent aux scénarios de test et de développement. Pour obtenir des conseils sur le choix de la méthode d’authentification appropriée, consultez les instructions d’authentification.

Options d’authentification

Cet article présente plusieurs méthodes d’authentification pour répondre à différents scénarios :

Pour les applications de production avec interaction utilisateur, utilisez l’authentification Microsoft Entra ID :

<PackageReference Include="Microsoft.TeamFoundationServer.Client" Version="19.225.1" />
<PackageReference Include="Microsoft.VisualStudio.Services.InteractiveClient" Version="19.225.1" />
<PackageReference Include="Microsoft.Identity.Client" Version="4.61.3" />

Pour les scénarios automatisés, les pipelines CI/CD et les applications serveur :

<PackageReference Include="Microsoft.TeamFoundationServer.Client" Version="19.225.1" />
<PackageReference Include="Microsoft.Identity.Client" Version="4.61.3" />

Pour les applications s’exécutant sur des services Azure (Functions, App Service, etc.) :

<PackageReference Include="Microsoft.TeamFoundationServer.Client" Version="19.225.1" />
<PackageReference Include="Azure.Identity" Version="1.10.4" />

Authentification par jeton d’accès personnel

Pour les scénarios de développement et de test :

<PackageReference Include="Microsoft.TeamFoundationServer.Client" Version="19.225.1" />

Exemples de code C#

Les exemples suivants montrent comment créer des éléments de travail à l’aide de différentes méthodes d’authentification.

Exemple 1 : Authentification avec Microsoft Entra ID (interactive)

// NuGet packages:
// Microsoft.TeamFoundationServer.Client
// Microsoft.VisualStudio.Services.InteractiveClient  
// Microsoft.Identity.Client
using System;
using System.Threading.Tasks;
using Microsoft.TeamFoundation.WorkItemTracking.WebApi;
using Microsoft.TeamFoundation.WorkItemTracking.WebApi.Models;
using Microsoft.VisualStudio.Services.Common;
using Microsoft.VisualStudio.Services.WebApi;
using Microsoft.VisualStudio.Services.WebApi.Patch;
using Microsoft.VisualStudio.Services.WebApi.Patch.Json;

public class EntraIdBugCreator
{
    private readonly Uri uri;

    /// <summary>
    /// Initializes a new instance using Microsoft Entra ID authentication.
    /// </summary>
    /// <param name="orgName">Your Azure DevOps organization name</param>
    public EntraIdBugCreator(string orgName)
    {
        this.uri = new Uri($"https://dev.azure.com/{orgName}");
    }

    /// <summary>
    /// Create a bug using Microsoft Entra ID authentication.
    /// </summary>
    /// <param name="project">The name of your project</param>
    /// <param name="title">Bug title</param>
    /// <param name="reproSteps">Reproduction steps</param>
    /// <param name="priority">Priority level (1-4)</param>
    /// <param name="severity">Severity level</param>
    /// <returns>The created WorkItem</returns>
    public async Task<WorkItem> CreateBugAsync(string project, string title, string reproSteps, int priority = 2, string severity = "3 - Medium")
    {
        // Use Microsoft Entra ID authentication
        var credentials = new VssAadCredential();
        var patchDocument = new JsonPatchDocument();

        // Add required and optional fields
        patchDocument.Add(new JsonPatchOperation()
        {
            Operation = Operation.Add,
            Path = "/fields/System.Title",
            Value = title
        });

        if (!string.IsNullOrEmpty(reproSteps))
        {
            patchDocument.Add(new JsonPatchOperation()
            {
                Operation = Operation.Add,
                Path = "/fields/Microsoft.VSTS.TCM.ReproSteps",
                Value = reproSteps
            });
        }

        patchDocument.Add(new JsonPatchOperation()
        {
            Operation = Operation.Add,
            Path = "/fields/Microsoft.VSTS.Common.Priority",
            Value = priority.ToString()
        });

        patchDocument.Add(new JsonPatchOperation()
        {
            Operation = Operation.Add,
            Path = "/fields/Microsoft.VSTS.Common.Severity",
            Value = severity
        });

        using (var connection = new VssConnection(this.uri, new VssCredentials(credentials)))
        {
            var workItemTrackingHttpClient = connection.GetClient<WorkItemTrackingHttpClient>();

            try
            {
                var result = await workItemTrackingHttpClient.CreateWorkItemAsync(patchDocument, project, "Bug").ConfigureAwait(false);
                Console.WriteLine($"Bug successfully created: Bug #{result.Id}");
                return result;
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Error creating bug: {ex.Message}");
                throw;
            }
        }
    }
}

Exemple 2 : Authentification du principal de service (scénarios automatisés)

// NuGet packages:
// Microsoft.TeamFoundationServer.Client
// Microsoft.Identity.Client
using System;
using System.Threading.Tasks;
using Microsoft.Identity.Client;
using Microsoft.TeamFoundation.WorkItemTracking.WebApi;
using Microsoft.TeamFoundation.WorkItemTracking.WebApi.Models;
using Microsoft.VisualStudio.Services.Common;
using Microsoft.VisualStudio.Services.WebApi;
using Microsoft.VisualStudio.Services.WebApi.Patch;
using Microsoft.VisualStudio.Services.WebApi.Patch.Json;

public class ServicePrincipalBugCreator
{
    private readonly Uri uri;
    private readonly string clientId;
    private readonly string clientSecret;
    private readonly string tenantId;

    /// <summary>
    /// Initializes a new instance using Service Principal authentication.
    /// </summary>
    /// <param name="orgName">Your Azure DevOps organization name</param>
    /// <param name="clientId">Service principal client ID</param>
    /// <param name="clientSecret">Service principal client secret</param>
    /// <param name="tenantId">Azure AD tenant ID</param>
    public ServicePrincipalBugCreator(string orgName, string clientId, string clientSecret, string tenantId)
    {
        this.uri = new Uri($"https://dev.azure.com/{orgName}");
        this.clientId = clientId;
        this.clientSecret = clientSecret;
        this.tenantId = tenantId;
    }

    /// <summary>
    /// Create a bug using Service Principal authentication.
    /// </summary>
    public async Task<WorkItem> CreateBugAsync(string project, string title, string reproSteps, int priority = 2, string severity = "3 - Medium")
    {
        // Acquire token using Service Principal
        var app = ConfidentialClientApplicationBuilder
            .Create(this.clientId)
            .WithClientSecret(this.clientSecret)
            .WithAuthority($"https://login.microsoftonline.com/{this.tenantId}")
            .Build();

        var scopes = new[] { "https://app.vssps.visualstudio.com/.default" };
        var result = await app.AcquireTokenForClient(scopes).ExecuteAsync();

        var credentials = new VssOAuthAccessTokenCredential(result.AccessToken);
        var patchDocument = new JsonPatchDocument();

        // Add work item fields
        patchDocument.Add(new JsonPatchOperation()
        {
            Operation = Operation.Add,
            Path = "/fields/System.Title",
            Value = title
        });

        if (!string.IsNullOrEmpty(reproSteps))
        {
            patchDocument.Add(new JsonPatchOperation()
            {
                Operation = Operation.Add,
                Path = "/fields/Microsoft.VSTS.TCM.ReproSteps",
                Value = reproSteps
            });
        }

        patchDocument.Add(new JsonPatchOperation()
        {
            Operation = Operation.Add,
            Path = "/fields/Microsoft.VSTS.Common.Priority",
            Value = priority.ToString()
        });

        patchDocument.Add(new JsonPatchOperation()
        {
            Operation = Operation.Add,
            Path = "/fields/Microsoft.VSTS.Common.Severity",
            Value = severity
        });

        using (var connection = new VssConnection(this.uri, new VssCredentials(credentials)))
        {
            var workItemTrackingHttpClient = connection.GetClient<WorkItemTrackingHttpClient>();

            try
            {
                var workItem = await workItemTrackingHttpClient.CreateWorkItemAsync(patchDocument, project, "Bug").ConfigureAwait(false);
                Console.WriteLine($"Bug successfully created: Bug #{workItem.Id}");
                return workItem;
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Error creating bug: {ex.Message}");
                throw;
            }
        }
    }
}

Exemple 3 : Authentification d’identité managée (applications hébergées par Azure)

// NuGet packages:
// Microsoft.TeamFoundationServer.Client
// Azure.Identity
using System;
using System.Threading.Tasks;
using Azure.Core;
using Azure.Identity;
using Microsoft.TeamFoundation.WorkItemTracking.WebApi;
using Microsoft.TeamFoundation.WorkItemTracking.WebApi.Models;
using Microsoft.VisualStudio.Services.Common;
using Microsoft.VisualStudio.Services.WebApi;
using Microsoft.VisualStudio.Services.WebApi.Patch;
using Microsoft.VisualStudio.Services.WebApi.Patch.Json;

public class ManagedIdentityBugCreator
{
    private readonly Uri uri;

    /// <summary>
    /// Initializes a new instance using Managed Identity authentication.
    /// </summary>
    /// <param name="orgName">Your Azure DevOps organization name</param>
    public ManagedIdentityBugCreator(string orgName)
    {
        this.uri = new Uri($"https://dev.azure.com/{orgName}");
    }

    /// <summary>
    /// Create a bug using Managed Identity authentication.
    /// </summary>
    public async Task<WorkItem> CreateBugAsync(string project, string title, string reproSteps, int priority = 2, string severity = "3 - Medium")
    {
        // Use Managed Identity to acquire token
        var credential = new DefaultAzureCredential();
        var tokenRequestContext = new TokenRequestContext(new[] { "https://app.vssps.visualstudio.com/.default" });
        var tokenResult = await credential.GetTokenAsync(tokenRequestContext);

        var credentials = new VssOAuthAccessTokenCredential(tokenResult.Token);
        var patchDocument = new JsonPatchDocument();

        // Add work item fields
        patchDocument.Add(new JsonPatchOperation()
        {
            Operation = Operation.Add,
            Path = "/fields/System.Title",
            Value = title
        });

        if (!string.IsNullOrEmpty(reproSteps))
        {
            patchDocument.Add(new JsonPatchOperation()
            {
                Operation = Operation.Add,
                Path = "/fields/Microsoft.VSTS.TCM.ReproSteps",
                Value = reproSteps
            });
        }

        patchDocument.Add(new JsonPatchOperation()
        {
            Operation = Operation.Add,
            Path = "/fields/Microsoft.VSTS.Common.Priority",
            Value = priority.ToString()
        });

        patchDocument.Add(new JsonPatchOperation()
        {
            Operation = Operation.Add,
            Path = "/fields/Microsoft.VSTS.Common.Severity",
            Value = severity
        });

        using (var connection = new VssConnection(this.uri, new VssCredentials(credentials)))
        {
            var workItemTrackingHttpClient = connection.GetClient<WorkItemTrackingHttpClient>();

            try
            {
                var workItem = await workItemTrackingHttpClient.CreateWorkItemAsync(patchDocument, project, "Bug").ConfigureAwait(false);
                Console.WriteLine($"Bug successfully created: Bug #{workItem.Id}");
                return workItem;
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Error creating bug: {ex.Message}");
                throw;
            }
        }
    }
}

Exemple 4 : Authentification par jeton d’accès personnel

// NuGet package: Microsoft.TeamFoundationServer.Client
using System;
using System.Threading.Tasks;
using Microsoft.TeamFoundation.WorkItemTracking.WebApi;
using Microsoft.TeamFoundation.WorkItemTracking.WebApi.Models;
using Microsoft.VisualStudio.Services.Common;
using Microsoft.VisualStudio.Services.WebApi;
using Microsoft.VisualStudio.Services.WebApi.Patch;
using Microsoft.VisualStudio.Services.WebApi.Patch.Json;

public class PatBugCreator
{
    private readonly Uri uri;
    private readonly string personalAccessToken;

    /// <summary>
    /// Initializes a new instance using Personal Access Token authentication.
    /// </summary>
    /// <param name="orgName">Your Azure DevOps organization name</param>
    /// <param name="personalAccessToken">Your Personal Access Token</param>
    public PatBugCreator(string orgName, string personalAccessToken)
    {
        this.uri = new Uri($"https://dev.azure.com/{orgName}");
        this.personalAccessToken = personalAccessToken;
    }

    /// <summary>
    /// Create a bug using Personal Access Token authentication.
    /// </summary>
    /// <param name="project">The name of your project</param>
    /// <param name="title">Bug title</param>
    /// <param name="reproSteps">Reproduction steps</param>
    /// <param name="priority">Priority level (1-4)</param>
    /// <param name="severity">Severity level</param>
    /// <returns>The created WorkItem</returns>
    public async Task<WorkItem> CreateBugAsync(string project, string title, string reproSteps, int priority = 2, string severity = "3 - Medium")
    {
        var credentials = new VssBasicCredential(string.Empty, this.personalAccessToken);
        var patchDocument = new JsonPatchDocument();

        // Add required and optional fields
        patchDocument.Add(new JsonPatchOperation()
        {
            Operation = Operation.Add,
            Path = "/fields/System.Title",
            Value = title
        });

        if (!string.IsNullOrEmpty(reproSteps))
        {
            patchDocument.Add(new JsonPatchOperation()
            {
                Operation = Operation.Add,
                Path = "/fields/Microsoft.VSTS.TCM.ReproSteps",
                Value = reproSteps
            });
        }

        patchDocument.Add(new JsonPatchOperation()
        {
            Operation = Operation.Add,
            Path = "/fields/Microsoft.VSTS.Common.Priority",
            Value = priority.ToString()
        });

        patchDocument.Add(new JsonPatchOperation()
        {
            Operation = Operation.Add,
            Path = "/fields/Microsoft.VSTS.Common.Severity",
            Value = severity
        });

        using (var connection = new VssConnection(this.uri, new VssCredentials(credentials)))
        {
            var workItemTrackingHttpClient = connection.GetClient<WorkItemTrackingHttpClient>();

            try
            {
                var result = await workItemTrackingHttpClient.CreateWorkItemAsync(patchDocument, project, "Bug").ConfigureAwait(false);
                Console.WriteLine($"Bug successfully created: Bug #{result.Id}");
                return result;
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Error creating bug: {ex.Message}");
                throw;
            }
        }
    }
}

Exemples d’utilisation

Utilisation de l’authentification Microsoft Entra ID (interactive)

class Program
{
    static async Task Main(string[] args)
    {
        var bugCreator = new EntraIdBugCreator("your-organization-name");
        
        var bug = await bugCreator.CreateBugAsync(
            project: "your-project-name",
            title: "Authorization Errors with Microsoft Accounts",
            reproSteps: "Our authorization logic needs to allow for users with Microsoft accounts (formerly Live IDs) - https://docs.microsoft.com/library/live/hh826547.aspx",
            priority: 1,
            severity: "2 - High"
        );
        
        Console.WriteLine($"Created bug with ID: {bug.Id}");
    }
}

Utilisation de l’authentification du principal de service (scénarios CI/CD)

class Program
{
    static async Task Main(string[] args)
    {
        // These values should come from environment variables or Azure Key Vault
        var clientId = Environment.GetEnvironmentVariable("AZURE_CLIENT_ID");
        var clientSecret = Environment.GetEnvironmentVariable("AZURE_CLIENT_SECRET");
        var tenantId = Environment.GetEnvironmentVariable("AZURE_TENANT_ID");
        
        var bugCreator = new ServicePrincipalBugCreator("your-organization-name", clientId, clientSecret, tenantId);
        
        var bug = await bugCreator.CreateBugAsync(
            project: "your-project-name",
            title: "Automated Bug Report",
            reproSteps: "Issue detected by automated testing...",
            priority: 2,
            severity: "3 - Medium"
        );
        
        Console.WriteLine($"Automated bug created: #{bug.Id}");
    }
}

Utilisation de l’authentification d’identité managée (Azure Functions/App Service)

public class BugReportFunction
{
    [FunctionName("CreateBugReport")]
    public static async Task<IActionResult> Run(
        [HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequest req,
        ILogger log)
    {
        var bugCreator = new ManagedIdentityBugCreator("your-organization-name");
        
        var bug = await bugCreator.CreateBugAsync(
            project: "your-project-name",
            title: "Function-detected Issue",
            reproSteps: "Issue reported via Azure Function...",
            priority: 3,
            severity: "4 - Low"
        );
        
        return new OkObjectResult($"Bug created: {bug.Id}");
    }
}

Utilisation de l’authentification par jeton d’accès personnel (développement/test)

class Program
{
    static async Task Main(string[] args)
    {
        var pat = Environment.GetEnvironmentVariable("AZURE_DEVOPS_PAT"); // Never hardcode PATs
        var bugCreator = new PatBugCreator("your-organization-name", pat);
        
        var bug = await bugCreator.CreateBugAsync(
            project: "your-project-name",
            title: "Sample Bug Title",
            reproSteps: "Steps to reproduce the issue...",
            priority: 2,
            severity: "3 - Medium"
        );
        
        Console.WriteLine($"Bug created successfully: #{bug.Id}");
    }
}

Référence du champ de l’élément de travail

Lors de la création d’éléments de travail, vous utiliserez généralement ces champs :

Champs obligatoires

  • System.Title : titre de l’élément de travail (requis pour tous les types d’éléments de travail)
  • System.WorkItemType : défini automatiquement lors de la spécification du type dans l’appel d’API

Champs facultatifs courants

  • Microsoft.VSTS.TCM.ReproSteps : étapes de reproduction détaillées
  • Microsoft.VSTS.Common.Priority : niveau de priorité (1=le plus élevé, 4=le plus bas)
  • Microsoft.VSTS.Common.Severity : classification de gravité
  • System.Description : Description générale ou détails supplémentaires
  • System.AssignedTo : personne responsable de l’élément de travail
  • System.AreaPath : Classification des zones
  • System.IterationPath: Affectation d’itération/sprint

Valeurs de priorité

  • 1 : Priorité critique/la plus élevée
  • 2 : Priorité élevée
  • 3 : Priorité moyenne (valeur par défaut)
  • 4 : Priorité faible

Valeurs de gravité courantes

  • 1 - Critique : Système inutilisable, blocage de la progression
  • 2 - Élevé : Fonctionnalités majeures rompues
  • 3 - Moyen : Certaines fonctionnalités sont rompues (par défaut)
  • 4 - Faible : Problèmes mineurs ou de nature cosmétique

Meilleures pratiques

Authentification

  • Utiliser l’ID Microsoft Entra pour les applications interactives avec la connexion utilisateur
  • Utiliser le principal de service pour les scénarios automatisés, les pipelines CI/CD et les applications serveur
  • Utiliser l’identité managée pour les applications s’exécutant sur des services Azure (Functions, App Service, machines virtuelles)
  • Évitez les jetons d’accès personnels en production ; utiliser uniquement pour le développement et les tests
  • Ne codez jamais en dur les informations d’identification dans le code source ; utiliser des variables d’environnement ou Azure Key Vault
  • Implémenter la rotation des informations d’identification pour les applications longues
  • Vérifier les étendues appropriées : la création d’un élément de travail nécessite des autorisations appropriées dans Azure DevOps

Gestion des erreurs

  • Implémenter une gestion appropriée des exceptions pour les échecs d’authentification et d’API
  • Valider les valeurs de champ avant de tenter de créer des éléments de travail
  • Gérer les erreurs de validation de champ retournées par l’API
  • Utiliser des modèles asynchrones/await pour améliorer la réactivité de l’application

Performances

  • Opérations par lots lors de la création de plusieurs éléments de travail
  • Mettre en cache les connexions lors de l’exécution de plusieurs appels d’API
  • Utiliser les valeurs de délai d’expiration appropriées pour les opérations de longue durée
  • Implémenter une logique de nouvelle tentative avec repli exponentiel pour les pannes transitoires

Validation des données

  • Valider les champs requis avant les appels d’API
  • Vérifier les autorisations de champ et les règles de type d’élément de travail
  • Nettoyer l’entrée utilisateur pour empêcher les attaques par injection
  • Respecter les exigences de champ spécifiques au projet et les conventions d’affectation de noms

Résolution des problèmes

Problèmes d’authentification

  • Échecs d’authentification d’ID Microsoft Entra : vérifiez que l’utilisateur dispose des autorisations appropriées pour créer des éléments de travail
  • Échecs d’authentification du principal de service : vérifiez que l’ID client, le secret et l’ID de locataire sont corrects ; vérifier les autorisations du principal de service dans Azure DevOps
  • Échecs d’authentification d’identité managée : vérifiez que la ressource Azure dispose d’une identité managée activée et d’autorisations appropriées
  • Échecs d’authentification PAT : vérifiez que le jeton a vso.work_write une portée et n’a pas expiré
  • Erreurs 403 Interdiction d’accès : vérifiez les permissions du projet et l’accès au type d’élément de travail.

Erreurs de validation de champ

  • Champ obligatoire manquant : vérifiez que tous les champs obligatoires sont inclus dans le document de correctif
  • Valeurs de champ non valides : vérifiez que les valeurs de champ correspondent au format attendu et aux valeurs autorisées
  • Champ introuvable : vérifiez que les noms de champs sont correctement orthographiés et existent pour le type d’élément de travail
  • Erreurs de champ en lecture seule : certains champs ne peuvent pas être définis lors de la création (par exemple, System.CreatedBy)

Exceptions courantes

  • VssUnauthorizedException : Échec de l’authentification ou autorisations insuffisantes
  • VssServiceException : erreurs de validation côté serveur ou problèmes d’API
  • ArgumentException : Document de correctif mal formé ou des paramètres non valides
  • JsonReaderException : Problèmes de sérialisation/désérialisation JSON

Problèmes de performance

  • Réponses lentes aux API : Vérifier la connectivité réseau et l’état du service Azure DevOps
  • Utilisation de la mémoire : supprimer correctement les connexions et les clients
  • Limitation du débit : implémenter des retards appropriés entre les appels d’API

Étapes suivantes