Use a decorator to inject steps into a pipeline

Note

Check out our newest documentation on extension development using the Azure DevOps Extension SDK.

Pipeline decorators let you add steps to the beginning and end of every job. This process is different than adding steps to a single definition because it applies to all pipelines in an organization.

Suppose our organization requires running a virus scanner on all build outputs that could be released. Pipeline authors don't need to remember to add that step. We create a decorator that automatically injects the step. Our pipeline decorator injects a custom task that does virus scanning at the end of every pipeline job.

Author a pipeline decorator

This example assumes you're familiar with the contribution models.

Start by creating an extension. After you follow the tutorial, you'll have a vss-extension.json file. In this file, add contribution for our new pipeline decorator.

vss-extension.json

{
    "manifestVersion": 1,
    "contributions": [
        {
            "id": "my-required-task",
            "type": "ms.azure-pipelines.pipeline-decorator",
            "targets": [
                "ms.azure-pipelines-agent-job.post-job-tasks"
            ],
            "properties": {
                "template": "my-decorator.yml"
            }
        }
    ],
    "files": [
        {
            "path": "my-decorator.yml",
            "addressable": true,
            "contentType": "text/plain"
        }
    ]
}

Let's take a look at the properties and what they're used for:

Property Description
id Contribution identifier. Must be unique among contributions in this extension.
type Specifies that this contribution is a pipeline decorator. Must be the string ms.azure-pipelines.pipeline-decorator.
targets Decorators can run before your job, after, or both. For executing the decorator before or after jobs in build or yaml pipelines, the targets are ms.azure-pipelines-agent-job.pre-job-tasks and ms.azure-pipelines-agent-job.post-job-tasks. ms.azure-release-pipelines-agent-job.pre-job-tasks and ms.azure-release-pipelines-agent-job.post-job-tasks targets inject the decorators in jobs in release pipelines. In this example, we use ms.azure-pipelines-agent-job.post-job-tasks only because we want to run at the end of all build jobs.
properties The only property required is a template. The template is a YAML file included in your extension, which defines the steps for your pipeline decorator. It's a relative path from the root of your extension folder.

This extension contributes a pipeline decorator. Next, we'll create a template YAML file to define the decorator's behavior.

Decorator YAML

In the extension's properties, we chose the name "my-decorator.yml". Create that file in the root of your contribution. It holds the set of steps to run after each job. We'll start with a basic example and work up to the full task.

my-decorator.yml (initial version)


steps:
  - task: CmdLine@2
    displayName: 'Run my script (injected from decorator)'
    inputs:
      script: dir

Installing the decorator

To add a pipeline decorator to your organization, you must install an extension. Only private extensions can contribute pipeline decorators. The extension must be authored and shared with your organization before it can be used.

Once the extension has been shared with your organization, search for the extension and install it.

Important

Pipeline decorators are in preview. You must enable the feature at the organization level Otherwise, pipeline decorators don't run.

Save the file, then build and install the extension. Create and run a basic pipeline. The decorator automatically injects our dir script at the end of every job. A pipeline run looks similar to:

Pipeline decorator running a simple script

Note

The decorator runs on every job in every pipeline in the organization. In later steps, we'll add logic to control when and how the decorator runs.

Conditional injection

In our example, we only need to run the virus scanner if the build outputs might be released to the public. Let's say that only builds from the default branch (typically master) are ever released. We should limit the decorator to jobs running against the default branch.

The updated file looks like this:

my-decorator.yml (revised version)


steps:
- ${{ if eq(resources.repositories['self'].ref, resources.repositories['self'].defaultBranch) }}:
  - script: dir
    displayName: 'Run my script (injected from decorator)'

You can start to see the power of this extensibility point. Use the context of the current job to conditionally inject steps at runtime. Use YAML expressions to make decisions about what steps to inject and when. See pipeline decorator expression context for a full list of available data.

There's another condition we need to consider: what if the user already included the virus scanning step? We shouldn't waste time running it again. In this simple example, we'll pretend that any script task found in the job is running the virus scanner. (In a real implementation, you'd have a custom task to check for that instead.)

The script task's ID is d9bafed4-0b18-4f58-968d-86655b4d2ce9. If we see another script task, we shouldn't inject ours.

my-decorator.yml (final version)


steps:
- ${{ if and(eq(resources.repositories['self'].ref, resources.repositories['self'].defaultBranch), not(containsValue(job.steps.*.task.id, 'd9bafed4-0b18-4f58-968d-86655b4d2ce9'))) }}:
  - script: dir
    displayName: 'Run my script (injected from decorator)'

Debugging

While authoring your pipeline decorator, you'll likely need to debug. You also may want to see what data you have available in the context.

You can set the system.debugContext variable to true when you queue a pipeline. Then, look at the pipeline summary page.

You see something similar to the following image:

View pipeline decorator context

Select the task to see the logs, which report the available context is available and runtime values.

Learn more about YAML expression syntax.