Azure Pipelines logo

Backstage Azure Pipelines Plugin

Created by Keyloop

Azure Pipelines is the CI CD service in Azure DevOps. It builds, tests, and deploys your code. It works with Azure Repos and other git providers. It runs on Microsoft hosted agents or on your own machines.

The Azure Pipelines Backstage plugin brings that build signal into your catalog. It shows the latest builds for a repo or a named pipeline. You can scan status at a glance. You can jump from a service page to the build details you care about. The same plugin can surface pull requests, git tags, and the repo readme so the context stays in one place.

Common use cases are simple. Give teams one screen to check build health across many services. Triage a failing run without hunting for links. Keep docs and code history next to the pipeline view. The plugin supports mono repos and multi org Azure DevOps setups. It can work with Backstage permissions so you keep CI data scoped to owners.

Installation Instructions

These instructions apply to self-hosted Backstage only.

Install the frontend package

  1. Add the package to your app

    Copy
    yarn --cwd packages/app add @backstage-community/plugin-azure-devops

Show Azure Pipelines in the entity page

If your entities use dev.azure.com/project-repo

  1. Update the entity page

    Copy
    // packages/app/src/components/catalog/EntityPage.tsx
    import React from 'react';
    import { EntitySwitch } from '@backstage/plugin-catalog';
    import {
      EntityAzurePipelinesContent,
      isAzureDevOpsAvailable,
    } from '@backstage-community/plugin-azure-devops';
    
    // For example in your CI CD section
    const cicdContent = (
      <EntitySwitch>
        <EntitySwitch.Case if={isAzureDevOpsAvailable}>
          <EntityAzurePipelinesContent defaultLimit={25} />
        </EntitySwitch.Case>
      </EntitySwitch>
    );

If your entities use dev.azure.com/project and dev.azure.com/build-definitionz

  1. Update the entity page

    Copy
    // packages/app/src/components/catalog/EntityPage.tsx
    import React from 'react';
    import { EntitySwitch } from '@backstage/plugin-catalog';
    import {
      EntityAzurePipelinesContent,
      isAzurePipelinesAvailable,
    } from '@backstage-community/plugin-azure-devops';
    
    // For example in your CI CD section
    const cicdContent = (
      <EntitySwitch>
        <EntitySwitch.Case if={isAzurePipelinesAvailable}>
          <EntityAzurePipelinesContent defaultLimit={25} />
        </EntitySwitch.Case>
      </EntitySwitch>
    );

Note
You can remove the if guard if you want the tab to always render
defaultLimit sets the max number of builds to show and defaults to ten

Configure Azure DevOps integration

  1. Add Azure DevOps credentials in your app config

    Copy
    integrations:
      azure:
        - host: dev.azure.com
          credentials:
            - organizations:
                - my-org
                - my-other-org
              clientId: ${AZURE_CLIENT_ID}
              clientSecret: ${AZURE_CLIENT_SECRET}
              tenantId: ${AZURE_TENANT_ID}
            - organizations:
                - another-org
              clientId: ${AZURE_CLIENT_ID}
        - host: server.company.com
          credentials:
            - organizations:
                - yet-another-org
              personalAccessToken: ${PERSONAL_ACCESS_TOKEN}

Add entity annotations

Use one of these setups

Repo with pipelines in the same project

Copy
# catalog-info.yaml
metadata:
  annotations:
    dev.azure.com/project-repo: my-project/my-repo

Monorepo with pipelines per entity

Copy
metadata:
  annotations:
    dev.azure.com/project-repo: my-project/my-repo
    dev.azure.com/build-definition: my-build-definition

If you want a readme for each entity in a monorepo

Copy
metadata:
  annotations:
    dev.azure.com/readme-path: /services/my-service/README.md

Note
The readme path must be absolute in the repo
Relative paths are not supported by the Azure DevOps API

Pipeline in a different project than the repo

Copy
metadata:
  annotations:
    dev.azure.com/project-repo: project-with-source-code/my-repo
    dev.azure.com/build-definition: my-build-definition
    dev.azure.com/project: project-with-build-code

Pipelines only with another SCM

Copy
metadata:
  annotations:
    dev.azure.com/project: my-project
    dev.azure.com/build-definition: "My Build With Spaces"

You can list more than one build definition separated by a comma

Multiple organizations

If you have more than one organization set in integrations azure add this

Copy
metadata:
  annotations:
    dev.azure.com/host-org: dev.azure.com/my-other-org

If you use a different host value in integrations azure match that host

Copy
metadata:
  annotations:
    dev.azure.com/host-org: server.company.com/yet-another-org

Names with spaces

Spaces in project and repo names are supported

Copy
metadata:
  annotations:
    dev.azure.com/project-repo: Has Spaces/With Space

Quotes are fine too

Copy
metadata:
  annotations:
    dev.azure.com/project: 'Has Spaces'

Install the backend plugin classic backend system

The backend plugin is required. Install it and mount its router.

  1. Add the backend package

    Copy
    yarn --cwd packages/backend add @backstage-community/plugin-azure-devops-backend
  2. Create the plugin router

    Copy
    // packages/backend/src/plugins/azureDevOps.ts
    import { Router } from 'express';
    import { createRouter } from '@backstage-community/plugin-azure-devops-backend';
    import { PluginEnvironment } from '../types';
    
    export default async function createPlugin(
      env: PluginEnvironment,
    ): Promise<Router> {
      return await createRouter({
        logger: env.logger,
        config: env.config,
        discovery: env.discovery,
        tokenManager: env.tokenManager,
        permissions: env.permissions,
        identity: env.identity,
      });
    }
  3. Mount the router

    Copy
    // packages/backend/src/index.ts
    import azureDevOps from './plugins/azureDevOps';
    
    const azureDevOpsEnv = useHotMemoize(module, () => createEnv('azure-devops'));
    
    async function main() {
      // ...
      const apiRouter = Router();
      // ...
      apiRouter.use(
        '/azure-devops',
        await azureDevOps(azureDevOpsEnv),
      );
      // ...
      await startStandaloneServer({ logger, router: apiRouter });
    }
    
    main().catch(error => {
      logger.error(error);
      process.exit(1);
    });

Note
The backend reads credentials from integrations azure in your config
Use the same config shown earlier

New backend system status

At this time the Azure DevOps backend plugin is provided as a classic router
Use the classic setup above in a new backend instance through the legacy router adapter if your app uses the new backend system

Example using the legacy adapter

Copy
// packages/backend/src/index.ts
import { createBackend } from '@backstage/backend-defaults';
import { createRouter } from '@backstage-community/plugin-azure-devops-backend';

const backend = createBackend();

backend.addLegacyRouter(async env => {
  return await createRouter({
    logger: env.logger,
    config: env.config,
    discovery: env.discovery,
    tokenManager: env.tokenManager,
    permissions: env.permissions,
    identity: env.identity,
  });
}, '/azure-devops');

backend.start();

If your installation uses a different bootstrap helper adapt the same idea
Mount the legacy router under a path such as /azure-devops

Optional catalog annotator processor

This processor can add dev.azure.com/host-org and dev.azure.com/project-repo for you

  1. Add the package

    Copy
    yarn --cwd packages/backend add @backstage-community/plugin-catalog-backend-module-azure-devops-annotator-processor
  2. Register the processor

    Copy
    // packages/backend/src/plugins/catalog.ts
    import { CatalogBuilder } from '@backstage/plugin-catalog-backend';
    import { AzureDevOpsAnnotatorProcessor } from '@backstage-community/plugin-catalog-backend-module-azure-devops-annotator-processor';
    
    export default async function createPlugin(env: PluginEnvironment) {
      const builder = await CatalogBuilder.create(env);
      builder.addProcessor(new AzureDevOpsAnnotatorProcessor(env.config));
      const { processingEngine, router } = await builder.build();
      await processingEngine.start();
      return router;
    }

Permission setup

The plugin supports the permission framework for pipelines and other features

  1. Add the common package to your backend where your permission policy lives

    Copy
    yarn --cwd packages/backend add @backstage-community/plugin-azure-devops-common
  2. Update your permission policy

    Copy
    // packages/backend/src/plugins/permission.ts or your policy file
    import {
      azureDevOpsPullRequestReadPermission,
      azureDevOpsPipelineReadPermission,
      azureDevOpsGitTagReadPermission,
      azureDevOpsReadmeReadPermission,
      azureDevOpsPullRequestDashboardReadPermission,
    } from '@backstage-community/plugin-azure-devops-common';
    import {
      AuthorizeResult,
      PolicyDecision,
      isPermission,
    } from '@backstage/plugin-permission-common';
    import {
      catalogConditions,
      createCatalogConditionalDecision,
    } from '@backstage/plugin-catalog-backend/alpha';
    
    export class MyPermissionPolicy /* implements PermissionPolicy */ {
      async handle(request: PolicyQuery, user?: BackstageIdentityResponse): Promise<PolicyDecision> {
        if (
          isPermission(request.permission, azureDevOpsPullRequestReadPermission) ||
          isPermission(request.permission, azureDevOpsPipelineReadPermission) ||
          isPermission(request.permission, azureDevOpsGitTagReadPermission) ||
          isPermission(request.permission, azureDevOpsReadmeReadPermission)
        ) {
          return createCatalogConditionalDecision(
            request.permission,
            catalogConditions.isEntityOwner({
              claims: user?.identity.ownershipEntityRefs ?? [],
            }),
          );
        }
    
        if (isPermission(request.permission, azureDevOpsPullRequestDashboardReadPermission)) {
          return { result: AuthorizeResult.ALLOW };
        }
    
        return { result: AuthorizeResult.ALLOW };
      }
    }

New frontend system alpha support

If you use the new frontend system you can enable the plugin as a feature

  1. Install the frontend package if you did not already

    Copy
    yarn --cwd packages/app add @backstage-community/plugin-azure-devops
  2. Register the feature

    Copy
    // packages/app-next or packages/app src App.tsx
    import azureDevOpsPlugin from '@backstage-community/plugin-azure-devops';
    
    export const app = createApp({
      features: [
        catalogPlugin,
        catalogImportPlugin,
        userSettingsPlugin,
        azureDevOpsPlugin,
      ],
    });

You can also enable feature discovery in app config

Copy
app:
  packages: all

Changelog

This changelog is produced from commits made to the Azure Pipelines plugin since 7 months ago, and based on the code located here. It may not contain information about all commits. Releases and version bumps are intentionally omitted. This changelog is generated by AI.

Features

  • Add actions to clone and push Azure DevOps repos. Register the actions in the scaffolder. Include examples and tests PR 4711 merged 23 days ago
  • Add action to create a pipeline from a YAML file PR 4229 merged 3 months ago
  • Add action to permit a pipeline to use protected resources. This automates resource authorizations after first run PR 4303 merged 2 months ago
  • Add option to wait for a pipeline to finish. Expose output variables when it completes PR 3651 merged 4 months ago
  • Add build run logs. Include API endpoint and a drawer to view logs PR 2559 merged 6 months ago
  • Add front end system filter for cards and tabs. Use availability helpers to show or hide UI PR 3489 merged 5 months ago
  • Export getAnnotationValuesFromEntity for use in other plugins PR 3261 merged 6 months ago

Bug fixes

  • Use strict filtering for build definitions when fetching builds. Prevent fuzzy matches PR 3829 merged 4 months ago
  • Validate the readme path annotation. Reject relative paths with a clear error since the Azure DevOps API does not support them PR 4232 merged 3 months ago

Deprecations

  • Deprecate getBuildDefinitions on the back end. It has no usages. It will be removed in a future release PR 4234 merged 3 months ago
  • Deprecate getRepoBuilds on the front end and back end. It has no usages. It will be removed in a future release PR 4231 merged 3 months ago
  • Move getAnnotationValuesFromEntity to the common package. The old location is deprecated and will be removed later PR 4236 merged 3 months ago

Maintenance

  • Update createFrontendPlugin usage to pluginId. Replace permissionIntegrationRouter with coreServices permissionsRegistry PR 4269 merged 3 months ago

Documentation

  • Add a note on how to set the build definition annotation and how to find the correct value in Azure DevOps PR 4233 merged 3 months ago
  • Update README with details about spaces in project and repo names PR 4235 merged 3 months ago
  • Update the permissions section in the README PR 4230 merged 3 months ago
  • Fix README links to point to the community plugins repo PR 3931 merged 4 months ago

Set up Backstage in minutes with Roadie