Bagikan melalui


Membuat bug di Azure DevOps Services menggunakan pustaka klien .NET

Layanan Azure DevOps | Azure DevOps Server | Azure DevOps Server 2022

Membuat item kerja secara terprogram adalah skenario otomatisasi umum di Azure DevOps Services. Artikel ini menunjukkan cara membuat bug (atau item kerja apa pun) menggunakan pustaka klien .NET dengan metode autentikasi modern.

Prasyarat

Kategori Persyaratan
Azure DevOps - Organisasi
- Akses ke proyek tempat Anda dapat membuat item kerja
Autentikasi Pilih salah satu metode berikut:
- Autentikasi ID Microsoft Entra (disarankan)
- Token Akses Pribadi (PAT) (untuk pengujian)
lingkungan Pengembangan Lingkungan pengembangan C#. Anda dapat menggunakan Visual Studio

Penting

Untuk aplikasi produksi, sebaiknya gunakan autentikasi ID Microsoft Entra alih-alih Token Akses Pribadi. Token Akses Pribadi (PAT) cocok untuk skenario pengujian dan pengembangan. Untuk panduan tentang memilih metode autentikasi yang tepat, lihat Panduan autentikasi.

Opsi autentikasi

Artikel ini menunjukkan beberapa metode autentikasi yang sesuai dengan skenario yang berbeda:

Untuk aplikasi produksi dengan interaksi pengguna, gunakan autentikasi ID Microsoft Entra:

<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" />

Untuk skenario otomatis, alur CI/CD, dan aplikasi server:

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

Untuk aplikasi yang berjalan di layanan Azure (Functions, App Service, dll.):

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

Autentikasi Token Akses Pribadi

Untuk skenario pengembangan dan pengujian:

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

Contoh kode C#

Contoh berikut menunjukkan cara membuat item kerja menggunakan metode autentikasi yang berbeda.

Contoh 1: Autentikasi ID Microsoft Entra (Interaktif)

// 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;
            }
        }
    }
}

Contoh 2: Autentikasi Perwakilan Layanan (Skenario otomatis)

// 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;
            }
        }
    }
}

Contoh 3: Autentikasi Identitas Terkelola (aplikasi yang dihosting 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;
            }
        }
    }
}

Contoh 4: Autentikasi Token Akses Pribadi

// 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;
            }
        }
    }
}

Contoh penggunaan

Menggunakan autentikasi ID Microsoft Entra (Interaktif)

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}");
    }
}

Menggunakan autentikasi Perwakilan Layanan (skenario 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}");
    }
}

Menggunakan autentikasi Identitas Terkelola (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}");
    }
}

Menggunakan autentikasi Token Akses Pribadi (Pengembangan/Pengujian)

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}");
    }
}

Referensi bidang item kerja

Saat membuat item kerja, Anda biasanya akan menggunakan bidang ini:

Bidang yang wajib diisi

  • System.Title: Judul item kerja (diperlukan untuk semua jenis item kerja)
  • System.WorkItemType: Secara otomatis diatur saat menentukan jenis dalam panggilan API

Bidang opsional umum

  • Microsoft.VSTS.TCM.ReproSteps: Langkah-langkah reproduksi terperinci
  • Microsoft.VSTS.Common.Priority: Tingkat prioritas (1=tertinggi, 4=terendah)
  • Microsoft.VSTS.Common.Severity: Klasifikasi tingkat keparahan
  • System.Description: Deskripsi umum atau detail tambahan
  • System.AssignedTo: Orang yang bertanggung jawab atas item kerja
  • System.AreaPath: Klasifikasi area
  • System.IterationPath: Perulangan/pembagian sprint

Nilai prioritas

  • 1: Prioritas Kritis/Tertinggi
  • 2: Prioritas tinggi
  • 3: Prioritas sedang (default)
  • 4: Prioritas rendah

Nilai tingkat keparahan umum

  • 1 - Kritis: Sistem tidak dapat digunakan, memblokir kemajuan
  • 2 - Tinggi: Fungsionalitas utama rusak
  • 3 - Sedang: Beberapa fungsionalitas rusak (default)
  • 4 - Rendah: Masalah kecil atau masalah kosmetik

Praktik terbaik

Otentikasi

  • Gunakan ID Microsoft Entra untuk aplikasi interaktif dengan pengguna masuk
  • Menggunakan Perwakilan Layanan untuk skenario otomatis, alur CI/CD, dan aplikasi server
  • Gunakan Identitas Terkelola untuk aplikasi yang berjalan di layanan Azure (Functions, App Service, VM)
  • Hindari Token Akses Pribadi dalam produksi; hanya digunakan untuk pengembangan dan pengujian
  • Jangan pernah meng-hardcode kredensial dalam kode sumber; gunakan variabel lingkungan atau Azure Key Vault
  • Menerapkan rotasi kredensial untuk aplikasi yang berjalan lama
  • Memastikan cakupan yang tepat: Pembuatan item kerja memerlukan izin yang sesuai di Azure DevOps

Penanganan kesalahan

  • Menerapkan penanganan pengecualian yang tepat untuk autentikasi dan kegagalan API
  • Memvalidasi nilai bidang sebelum mencoba membuat item kerja
  • Menangani kesalahan validasi dari bidang yang dikembalikan oleh API
  • Gunakan pola asinkron/tunggu untuk respons aplikasi yang lebih baik

Penampilan

  • Operasi batch saat membuat beberapa item kerja
  • Koneksi cache saat melakukan beberapa panggilan API
  • Gunakan nilai batas waktu yang sesuai untuk operasi yang berjalan lama
  • Menerapkan logika pengulangan dengan backoff eksponensial untuk kegagalan sementara

Validasi Data

  • Memvalidasi bidang yang diperlukan sebelum panggilan API
  • Memeriksa izin akses bidang dan aturan tipe item pekerjaan
  • Bersihkan input pengguna untuk mencegah serangan injeksi
  • Ikuti persyaratan bidang khusus proyek dan konvensi penamaan

Penyelesaian Masalah

Masalah autentikasi

  • Kegagalan autentikasi ID Microsoft Entra: Pastikan pengguna memiliki izin yang tepat untuk membuat item kerja
  • Kegagalan autentikasi Perwakilan Layanan: Verifikasi ID klien, rahasia, dan ID penyewa sudah benar; periksa izin perwakilan layanan di Azure DevOps
  • Kegagalan autentikasi Identitas Terkelola: Pastikan sumber daya Azure mengaktifkan identitas terkelola dan izin yang tepat
  • Kegagalan autentikasi PAT: Verifikasi bahwa token memiliki vso.work_write cakupan dan belum kedaluwarsa
  • 403 Kesalahan terlarang: Periksa izin proyek dan akses jenis item kerja

Kesalahan Validasi bidang

  • Bidang yang diperlukan hilang: Pastikan semua bidang yang diperlukan disertakan dalam dokumen patch
  • Nilai bidang tidak valid: Memverifikasi nilai bidang cocok dengan format yang diharapkan dan nilai yang diizinkan
  • Bidang tidak ditemukan: Pastikan nama bidang dieja dengan benar dan tersedia sesuai tipe item kerja.
  • Kesalahan kolom hanya-baca: Beberapa kolom tidak dapat diatur saat pembuatan (misalnya, System.CreatedBy)

Pengecualian umum

  • VssUnauthorizedException: Autentikasi gagal atau izin tidak memadai
  • VssServiceException: Kesalahan validasi sisi server atau masalah API
  • ArgumentException: Parameter tidak valid atau dokumen patch salah bentuk
  • JsonReaderException: Masalah dengan serialisasi/deserialisasi JSON

Masalah performa

  • Respons API lambat: Periksa konektivitas jaringan dan status layanan Azure DevOps
  • Penggunaan memori: Membuang koneksi dan klien dengan benar
  • Pembatasan tarif: Menerapkan penundaan yang sesuai antara panggilan API

Langkah berikutnya