How I Built a Build Information Dashboard in Azure DevOps

A delivery story about the visibility gap I kept hitting, the dashboard I built, and the release questions it stopped for the team.

What I Needed to Solve

Azure DevOps gave me pipeline and release details, but it never gave me one clear screen showing what build is deployed to each environment right now for each pipeline. This is the gap I kept running into across delivery reviews, support calls, and release conversations.

Important: this exact matrix-style deployment dashboard is not built in as a single native view in Azure DevOps, so we build it using Azure DevOps APIs.

What I Kept Seeing

I was regularly asked the same question across projects: what exactly is deployed to each environment right now? Azure DevOps exposed the raw release data, but it did not give me the one-screen answer I needed for support calls, release reviews, and incident conversations.

I built the dashboard so the answer became obvious in seconds instead of minutes.

What Changed for the Team

What the Dashboard Needed to Show

How I Built It

  1. Backend service calls Azure DevOps APIs using PAT.
  2. Fetch release definitions, releases, and deployments.
  3. Flatten response into dashboard model: pipeline -> environment -> latest deployment.
  4. Cache data for 30-120 seconds to avoid API throttling.
  5. UI renders matrix with status colors and build links.

Code I Used for the First Cut

The first version stayed small: one endpoint to fetch the deployment snapshot, one service to call Azure DevOps, and one model that the UI could render directly.

Data Panel
Reference table for quick scanning and comparison.
Reference Live Status
PiecePurpose
DashboardOptionsStores org, project, PAT, and cache settings.
AzureDevOpsClientCalls release APIs and returns flattened deployment data.
DashboardControllerExposes the refresh endpoint used by the UI refresh button.
public sealed class DashboardOptions
{
    public string Organization { get; set; } = "";
    public string Project { get; set; } = "";
    public string PersonalAccessToken { get; set; } = "";
    public int CacheSeconds { get; set; } = 60;
}
public sealed record DeploymentCell(string Environment, string Build, string Status, DateTimeOffset DeployedOnUtc);
public sealed record DashboardRow(string Pipeline, IReadOnlyList<DeploymentCell> Environments);
app.MapGet("/api/dashboard/deployments", async (AzureDevOpsClient client) =>
{
    var rows = await client.GetDashboardRowsAsync();
    return Results.Ok(new { generatedAtUtc = DateTimeOffset.UtcNow, rows });
});

Refresh Strategy

I deliberately did not make this a real-time dashboard. The page shows a last-refreshed indicator and a refresh button so the team can pull the latest state when they need it.

This mattered because the goal was visibility, not live telemetry.

Azure DevOps APIs I Used

Data Panel
Reference table for quick scanning and comparison.
Reference Live Status
PurposeEndpointNotes
List release definitionsGET https://vsrm.dev.azure.com/{org}/{project}/_apis/release/definitions?api-version=7.1Get dashboard rows
List releasesGET https://vsrm.dev.azure.com/{org}/{project}/_apis/release/releases?$expand=environments&$top=100&api-version=7.1Get environment-level release info
List deploymentsGET https://vsrm.dev.azure.com/{org}/{project}/_apis/release/deployments?latestAttemptsOnly=true&$top=100&api-version=7.1Get latest deployment attempts/status

Use continuation tokens for pagination when volume is high.

How I Authenticated

Create a PAT with minimum scope for reading release data (for example: release read). Send Basic auth with empty username and PAT as password.

Core Data Shape

{
  "pipelineName": "OrderService-Release",
  "environments": {
    "DEV":  { "build": "2026.06.01.4", "status": "succeeded", "deployedOn": "2026-06-01T08:12:00Z" },
    "QA":   { "build": "2026.05.31.9", "status": "succeeded", "deployedOn": "2026-05-31T16:40:00Z" },
    "PROD": { "build": "2026.05.28.2", "status": "partiallySucceeded", "deployedOn": "2026-05-28T22:10:00Z" }
  }
}

Build this flattened structure once, and your UI becomes very simple.

Build Steps

  1. Fetch release definitions.
  2. Fetch releases with environments expanded.
  3. Fetch deployments for latest attempts only.
  4. Group by definition + environment and keep most recent deployment.
  5. Attach build/version info from artifacts.
  6. Render matrix and stale indicators (e.g., older than 7 days).

Reference Implementation: API Call Sequence

  1. Call release definitions API to get the list of services/pipelines.
  2. Call releases API with $expand=environments to get environment-level status.
  3. Call deployments API with latestAttemptsOnly=true for latest execution state.
  4. If response has continuation token, keep calling until all pages are collected.
  5. Merge data and keep the most recent deployment per environment.

Reference Implementation: Backend Endpoint Contract

Create one backend endpoint such as GET /api/dashboard/deployments so UI does not call Azure DevOps directly.

{
  "generatedAtUtc": "2026-06-01T09:10:00Z",
  "source": "azure-devops",
  "rows": [
    {
      "pipeline": "OrderService-Release",
      "environments": [
        { "name": "DEV", "build": "2026.06.01.4", "status": "succeeded", "deployedOnUtc": "2026-06-01T08:12:00Z", "releaseId": 1021 },
        { "name": "QA", "build": "2026.05.31.9", "status": "succeeded", "deployedOnUtc": "2026-05-31T16:40:00Z", "releaseId": 1014 },
        { "name": "PROD", "build": "2026.05.28.2", "status": "partiallySucceeded", "deployedOnUtc": "2026-05-28T22:10:00Z", "releaseId": 995 }
      ]
    }
  ]
}

Reference Implementation: Environment Name Mapping

Environment names differ across teams (Dev, Development, QA-Internal, etc.). Normalize names before rendering the matrix.

Data Panel
Reference table for quick scanning and comparison.
Reference Live Status
Incoming NameNormalized Name
dev, development, d1DEV
qa, test, qualityQA
uat, preprod, stagingUAT
prod, production, livePROD

Reference Implementation: Manual Refresh and Caching

public sealed class DashboardCache
{
    private DashboardRow[]? _rows;
    private DateTimeOffset _expiresAtUtc;

    public bool TryGet(out DashboardRow[]? rows)
    {
        if (_rows is null || DateTimeOffset.UtcNow > _expiresAtUtc)
        {
            rows = null;
            return false;
        }

        rows = _rows;
        return true;
    }

    public void Set(DashboardRow[] rows, TimeSpan ttl)
    {
        _rows = rows;
        _expiresAtUtc = DateTimeOffset.UtcNow.Add(ttl);
    }
}

Where It Commonly Breaks

Delivery Hardening

Real Scenario: Latest Build Looks Stale Until Refresh

The dashboard shows QA updated correctly, but PROD still displays an older build. The release exists in Azure DevOps, yet the UI looks one cycle behind until someone clicks refresh.

Data Panel
Reference table for quick scanning and comparison.
Reference Live Status
SymptomWhat It Usually MeansWhat I Check First
One environment staleMapping or refresh issueEnvironment normalization and cache expiry
Some pipelines missingPagination not completedContinuation tokens and API page size
UI not updating after deploymentSnapshot not refreshed yetCache TTL, refresh trigger, timestamp shown on page

The usual fix is not a UI redesign. It is a data correctness issue in pagination, mapping, cache freshness, or simply waiting for the next manual refresh.

© 2026 Anup Kumar Chandrakumaran