A RetroSearch Logo

Home - News ( United States | United Kingdom | Italy | Germany ) - Football scores

Search Query:

Showing content from https://docs.microsoft.com/en-us/vsts/extend/develop/add-build-task below:

Add a Custom Build or Release Task in an Extension - Azure DevOps

Azure DevOps Services | Azure DevOps Server 2022 - Azure DevOps Server 2019

This guide walks you through creating, testing, and publishing custom build or release tasks as Azure DevOps extensions. Custom pipeline tasks let you extend Azure DevOps with specialized functionality tailored to your team's workflows, from simple utilities to complex integrations with external systems.

Learn how to do the following tasks:

For more information about Azure Pipelines, see What is Azure Pipelines?

Note

This article covers agent tasks in agent-based extensions. For information about server tasks and server-based extensions, see Server Task Authoring.

Prerequisites

Before you begin, ensure you have the following requirements in place:

Component Requirement Description Azure DevOps organization Required Create an organization if you don't have one Text editor Recommended Visual Studio Code for IntelliSense and debugging support Node.js Required Install the latest version (Node.js 20 or later recommended) TypeScript compiler Required Install the latest version (version 4.6.3 or later) Azure DevOps CLI (tfx-cli) Required Install using npm i -g tfx-cli to package extensions Azure DevOps Extension SDK Required Install the azure-devops-extension-sdk package Testing framework Required Mocha for unit testing (installed during setup) Project structure

Create a home directory for your project. After you complete this tutorial, your extension should have the following structure:

|--- README.md    
|--- images                        
    |--- extension-icon.png  
|--- buildandreleasetask            // Task scripts location
    |--- task.json                  // Task definition
    |--- index.ts                   // Main task logic
    |--- package.json               // Node.js dependencies
    |--- tests/                     // Unit tests
        |--- _suite.ts
        |--- success.ts
        |--- failure.ts
|--- vss-extension.json             // Extension manifest

Important

Your development machine must run the latest version of Node.js to ensure compatibility with the production environment. Update your task.json file to use Node 20:

"execution": {
    "Node20_1": {
      "target": "index.js"
    }
}
1. Create a custom task

This section guides you through creating the basic structure and implementation of your custom task. All files in this step should be created within the buildandreleasetask folder inside your project's home directory.

Note

This walkthrough uses Windows with PowerShell. The steps work on all platforms, but environment variable syntax differs. On Mac or Linux, replace $env:<var>=<val> with export <var>=<val>.

Set up the task scaffolding

Create the basic project structure and install required dependencies:

  1. To initialize the Node.js project, open PowerShell, go to your buildandreleasetask folder, and run:

    npm init --yes
    

    The package.json file gets created with default settings. The --yes flag accepts all default options automatically.

    Tip

    Azure Pipelines agents expect task folders to include node modules. Copy node_modules to your buildandreleasetask folder. To manage VSIX file size (50-MB limit), consider running npm install --production or npm prune --production before packaging.

  2. Install the Azure Pipelines Task Library:

    npm install azure-pipelines-task-lib --save
    
  3. Install TypeScript type definitions:

    npm install @types/node --save-dev
    npm install @types/q --save-dev
    
  4. Set up version control exclusions

    echo node_modules > .gitignore
    

    Your build process should run npm install to rebuild node_modules each time.

  5. Install testing dependencies:

    npm install mocha --save-dev -g
    npm install sync-request --save-dev
    npm install @types/mocha --save-dev
    
  6. Install TypeScript compiler:

    npm install typescript@4.6.3 -g --save-dev
    

    Note

    Install TypeScript globally to ensure the tsc command is available. Without it, TypeScript 2.3.4 is used by default.

  7. Configure TypeScript compilation:

    tsc --init --target es2022
    

    The tsconfig.json file gets created with ES2022 target settings.

Implement the task logic

With scaffolding complete, create the core task files that define functionality and metadata:

  1. Create the task definition file: Create task.json in the buildandreleasetask folder. This file describes your task to the Azure Pipelines system, defining inputs, execution settings, and UI presentation.

    {
     "$schema": "https://raw.githubusercontent.com/Microsoft/azure-pipelines-task-lib/master/tasks.schema.json",
     "id": "{{taskguid}}",
     "name": "{{taskname}}",
     "friendlyName": "{{taskfriendlyname}}",
     "description": "{{taskdescription}}",
     "helpMarkDown": "",
     "category": "Utility",
     "author": "{{taskauthor}}",
     "version": {
         "Major": 0,
         "Minor": 1,
         "Patch": 0
     },
     "instanceNameFormat": "Echo $(samplestring)",
     "inputs": [
         {
             "name": "samplestring",
             "type": "string",
             "label": "Sample String",
             "defaultValue": "",
             "required": true,
             "helpMarkDown": "A sample string"
         }
     ],
     "execution": {
         "Node20_1": {
             "target": "index.js"
         }
     }
     }
    

    Note

    Replace {{placeholders}} with your task's actual information. The taskguid must be unique. Generate one using PowerShell: (New-Guid).Guid

  2. To implement the task logic, create index.ts with your task's main functionality:

    import tl = require('azure-pipelines-task-lib/task');
    
     async function run() {
         try {
             const inputString: string | undefined = tl.getInput('samplestring', true);
             if (inputString == 'bad') {
                 tl.setResult(tl.TaskResult.Failed, 'Bad input was given');
                 return;
             }
             console.log('Hello', inputString);
         }
         catch (err: any) {
             tl.setResult(tl.TaskResult.Failed, err.message);
         }
     }
    
     run();
    
  3. Compile TypeScript to JavaScript:

    tsc
    

    The index.js file gets created from your TypeScript source.

Understanding task.json components

The task.json file is the heart of your task definition. Here are the key properties:

Property Description Example id Unique GUID identifier for your task Generated using (New-Guid).Guid name Task name without spaces (used internally) MyCustomTask friendlyName Display name shown in the UI My Custom Task description Detailed description of task functionality Performs custom operations on files author Publisher or author name My Company instanceNameFormat How the task appears in pipeline steps Process $(inputFile) inputs Array of input parameters See the following input types execution Execution environment specification Node20_1, PowerShell3, etc. restrictions Security restrictions for commands and variables Recommended for new tasks Security restrictions

For production tasks, add security restrictions to limit command usage and variable access:

"restrictions": {
  "commands": {
    "mode": "restricted"
  },
  "settableVariables": {
    "allowed": ["variable1", "test*"]
  }
}

Restricted mode allows only these commands:

Variable allowlist controls which variables can be set via setvariable or prependpath. Supports basic regex patterns.

Input types and examples

Common input types for task parameters:

"inputs": [
    {
        "name": "stringInput",
        "type": "string",
        "label": "Text Input",
        "defaultValue": "",
        "required": true,
        "helpMarkDown": "Enter a text value"
    },
    {
        "name": "boolInput",
        "type": "boolean",
        "label": "Enable Feature",
        "defaultValue": "false",
        "required": false
    },
    {
        "name": "picklistInput",
        "type": "pickList",
        "label": "Select Option",
        "options": {
            "option1": "First Option",
            "option2": "Second Option"
        },
        "defaultValue": "option1"
    },
    {
        "name": "fileInput",
        "type": "filePath",
        "label": "Input File",
        "required": true,
        "helpMarkDown": "Path to the input file"
    }
]
Test your task locally

Before packaging, test your task to ensure it works correctly:

  1. Test with missing input (should fail):

    node index.js
    

    Expected output:

    ##vso[task.debug]agent.workFolder=undefined
    ##vso[task.debug]loading inputs and endpoints
    ##vso[task.debug]loaded 0
    ##vso[task.debug]task result: Failed
    ##vso[task.issue type=error;]Input required: samplestring
    ##vso[task.complete result=Failed;]Input required: samplestring
    
  2. Test with valid input (should succeed):

    $env:INPUT_SAMPLESTRING="World"
    node index.js
    

    Expected output:

    ##vso[task.debug]agent.workFolder=undefined
    ##vso[task.debug]loading inputs and endpoints
    ##vso[task.debug]loading INPUT_SAMPLESTRING
    ##vso[task.debug]loaded 1
    ##vso[task.debug]samplestring=World
    Hello World
    
  3. Test error handling:

    $env:INPUT_SAMPLESTRING="bad"
    node index.js
    

    This action should trigger the error handling path in your code.

For more information, see the Build/release task reference.

2. Implement comprehensive unit testing

Testing your task thoroughly ensures reliability and helps catch issues before deployment to production pipelines.

Install testing dependencies

Install the required testing tools:

npm install mocha --save-dev -g
npm install sync-request --save-dev
npm install @types/mocha --save-dev
Create test
  1. Create a tests folder in your task directory containing a _suite.ts file:

     import * as path from 'path';
     import * as assert from 'assert';
     import * as ttm from 'azure-pipelines-task-lib/mock-test';
    
     describe('Sample task tests', function () {
    
         before( function() {
             // Setup before tests
         });
    
         after(() => {
             // Cleanup after tests
         });
    
         it('should succeed with simple inputs', function(done: Mocha.Done) {
             // Success test implementation
         });
    
         it('should fail if tool returns 1', function(done: Mocha.Done) {
             // Failure test implementation
         });    
       });
    

    Tip

    Your test folder should be located in the task folder (for example, buildandreleasetask). If you encounter a sync-request error, install it in the task folder: npm i --save-dev sync-request.

  2. Create success.ts in your test directory to simulate successful task execution:

     import ma = require('azure-pipelines-task-lib/mock-answer');
     import tmrm = require('azure-pipelines-task-lib/mock-run');
     import path = require('path');
    
     let taskPath = path.join(__dirname, '..', 'index.js');
     let tmr: tmrm.TaskMockRunner = new tmrm.TaskMockRunner(taskPath);
    
     // Set valid input for success scenario
     tmr.setInput('samplestring', 'human');
    
     tmr.run();
    
  3. Add the success test to your _suite.ts file:

     it('should succeed with simple inputs', function(done: Mocha.Done) {
         this.timeout(1000);
    
         let tp: string = path.join(__dirname, 'success.js');
         let tr: ttm.MockTestRunner = new ttm.MockTestRunner(tp);
    
         tr.runAsync().then(() => {
             console.log(tr.succeeded);
             assert.equal(tr.succeeded, true, 'should have succeeded');
             assert.equal(tr.warningIssues.length, 0, "should have no warnings");
             assert.equal(tr.errorIssues.length, 0, "should have no errors");
             console.log(tr.stdout);
             assert.equal(tr.stdout.indexOf('Hello human') >= 0, true, "should display Hello human");
             done();
         }).catch((error) => {
             done(error); // Ensure the test case fails if there's an error
         });
     });
    
  4. Create failure.ts in your test directory to test error handling:

    import ma = require('azure-pipelines-task-lib/mock-answer');
    import tmrm = require('azure-pipelines-task-lib/mock-run');
    import path = require('path');
    
    let taskPath = path.join(__dirname, '..', 'index.js');
    let tmr: tmrm.TaskMockRunner = new tmrm.TaskMockRunner(taskPath);
    
    // Set invalid input to trigger failure
    tmr.setInput('samplestring', 'bad');
    
    tmr.run();
    
  5. Add the failure test to your _suite.ts file:

     it('should fail if tool returns 1', function(done: Mocha.Done) {
         this.timeout(1000);
    
         const tp = path.join(__dirname, 'failure.js');
         const tr: ttm.MockTestRunner = new ttm.MockTestRunner(tp);
    
         tr.runAsync().then(() => {
             console.log(tr.succeeded);
             assert.equal(tr.succeeded, false, 'should have failed');
             assert.equal(tr.warningIssues.length, 0, 'should have no warnings');
             assert.equal(tr.errorIssues.length, 1, 'should have 1 error issue');
             assert.equal(tr.errorIssues[0], 'Bad input was given', 'error issue output');
             assert.equal(tr.stdout.indexOf('Hello bad'), -1, 'Should not display Hello bad');
             done();
         });
     });
    
Run your tests

Execute the test suite:

# Compile TypeScript
tsc

# Run tests
mocha tests/_suite.js

Both tests should pass. For verbose output (similar to build console output), set the trace environment variable:

$env:TASK_TEST_TRACE=1
mocha tests/_suite.js
Test coverage best practices Security best practices

After testing your task locally and implementing comprehensive unit tests, package it into an extension for Azure DevOps.

Install packaging tools

Install the Cross Platform Command Line Interface (tfx-cli):

npm install -g tfx-cli
Create the extension manifest

The extension manifest (vss-extension.json) contains all information about your extension, including references to your task folders and images.

  1. Create an images folder with an extension-icon.png file

  2. Create vss-extension.json in your extension's root directory (not in the task folder):

    {
     "manifestVersion": 1,
     "id": "my-custom-tasks",
     "name": "My Custom Tasks",
     "version": "1.0.0",
     "publisher": "your-publisher-id",
     "targets": [
         {
             "id": "Microsoft.VisualStudio.Services"
         }
     ],
     "description": "Custom build and release tasks for Azure DevOps",
     "categories": [
         "Azure Pipelines"
     ],
     "icons": {
         "default": "images/extension-icon.png"
     },
     "files": [
         {
             "path": "MyCustomTask"
         }
     ],
     "contributions": [
         {
             "id": "my-custom-task",
             "type": "ms.vss-distributed-task.task",
             "targets": [
                 "ms.vss-distributed-task.tasks"
             ],
             "properties": {
                 "name": "MyCustomTask"
             }
         }
     ]
    }
    
Key manifest properties Property Description publisher Your marketplace publisher identifier contributions.id Unique identifier within the extension contributions.properties.name Must match your task folder name files.path Path to your task folder relative to the manifest

Note

Change the publisher value to your publisher name. For information about creating a publisher, see Create your publisher.

Package your extension

Package your extension into a .vsix file:

tfx extension create --manifest-globs vss-extension.json
Version management
tfx extension create --manifest-globs vss-extension.json --rev-version

Important

Both the task version and extension version must be updated for changes to take effect in Azure DevOps.

Versioning strategy

Follow semantic versioning principles for your task updates:

Update process:

  1. Update task.json version
  2. Update vss-extension.json version
  3. Test thoroughly in a test organization
  4. Publish and monitor for issues
Publish to Visual Studio Marketplace 1. Create your publisher
  1. Sign in to the Visual Studio Marketplace Publishing Portal
  2. Create a new publisher if prompted:
  3. Review and accept the Marketplace Publisher Agreement
2. Upload your extension

Web interface method:

  1. Select Upload new extension
  2. Choose your packaged .vsix file
  3. Select Upload

Command-line method:

tfx extension publish --manifest-globs vss-extension.json --share-with yourOrganization
  1. Right-click your extension in the marketplace
  2. Select Share
  3. Enter your organization name
  4. Add more organizations as needed

Important

Publishers must be verified to share extensions publicly. For more information, see Package/Publish/Install.

4. Install to your organization

After sharing, install the extension to your Azure DevOps organization:

  1. Navigate to Organization Settings > Extensions
  2. Browse for your extension
  3. Select Get it free and install
3. Package and publish your extension Verify your extension

After installation, verify your task works correctly:

  1. Create or edit a pipeline.
  2. Add your custom task:
  3. Configure task parameters:
  4. Run the pipeline to test functionality
  5. Monitor execution:
4. Automate extension publishing with CI/CD

To maintain your custom task effectively, create automated build and release pipelines that handle testing, packaging, and publishing.

Prerequisites for automation Complete CI/CD pipeline

Create a YAML pipeline with comprehensive stages for testing, packaging, and publishing:

trigger: 
- main

pool:
  vmImage: "ubuntu-latest"

variables:
  - group: extension-variables # Your variable group name

stages:
  - stage: Test_and_validate
    displayName: 'Run Tests and Validate Code'
    jobs:
      - job: RunTests
        displayName: 'Execute unit tests'
        steps:
          - task: TfxInstaller@4
            displayName: 'Install TFX CLI'
            inputs:
              version: "v0.x"
          
          - task: Npm@1
            displayName: 'Install task dependencies'
            inputs:
              command: 'install'
              workingDir: '/MyCustomTask' # Update to your task directory
          
          - task: Bash@3
            displayName: 'Compile TypeScript'
            inputs:
              targetType: "inline"
              script: |
                cd MyCustomTask # Update to your task directory
                tsc
          
          - task: Npm@1
            displayName: 'Run unit tests'
            inputs:
              command: 'custom'
              workingDir: '/MyCustomTask' # Update to your task directory
              customCommand: 'test' # Ensure this script exists in package.json
          
          - task: PublishTestResults@2
            displayName: 'Publish test results'
            inputs:
              testResultsFormat: 'JUnit'
              testResultsFiles: '**/test-results.xml'
              searchFolder: '$(System.DefaultWorkingDirectory)'

  - stage: Package_extension
    displayName: 'Package Extension'
    dependsOn: Test_and_validate
    condition: succeeded()
    jobs:
      - job: PackageExtension
        displayName: 'Create VSIX package'
        steps:
          - task: TfxInstaller@4
            displayName: 'Install TFX CLI'
            inputs:
              version: "v0.x"
          
          - task: Npm@1
            displayName: 'Install dependencies'
            inputs:
              command: 'install'
              workingDir: '/MyCustomTask'
          
          - task: Bash@3
            displayName: 'Compile TypeScript'
            inputs:
              targetType: "inline"
              script: |
                cd MyCustomTask
                tsc
          
          - task: QueryAzureDevOpsExtensionVersion@4
            name: QueryVersion
            displayName: 'Query current extension version'
            inputs:
              connectTo: 'VsTeam'
              connectedServiceName: 'marketplace-connection'
              publisherId: '$(publisherId)'
              extensionId: '$(extensionId)'
              versionAction: 'Patch'
          
          - task: PackageAzureDevOpsExtension@4
            displayName: 'Package extension'
            inputs:
              rootFolder: '$(System.DefaultWorkingDirectory)'
              publisherId: '$(publisherId)'
              extensionId: '$(extensionId)'
              extensionName: '$(extensionName)'
              extensionVersion: '$(QueryVersion.Extension.Version)'
              updateTasksVersion: true
              updateTasksVersionType: 'patch'
              extensionVisibility: 'private'
              extensionPricing: 'free'
          
          - task: PublishBuildArtifacts@1
            displayName: 'Publish VSIX artifact'
            inputs:
              PathtoPublish: '$(System.DefaultWorkingDirectory)/*.vsix'
              ArtifactName: '$(artifactName)'
              publishLocation: 'Container'

  - stage: Publish_to_marketplace
    displayName: 'Publish to Marketplace'
    dependsOn: Package_extension
    condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
    jobs:
      - deployment: PublishExtension
        displayName: 'Deploy to marketplace'
        environment: 'marketplace-production'
        strategy:
          runOnce:
            deploy:
              steps:
                - task: TfxInstaller@4
                  displayName: 'Install TFX CLI'
                  inputs:
                    version: "v0.x"
                
                - task: PublishAzureDevOpsExtension@4
                  displayName: 'Publish to marketplace'
                  inputs:
                    connectTo: 'VsTeam'
                    connectedServiceName: 'marketplace-connection'
                    fileType: 'vsix'
                    vsixFile: '$(Pipeline.Workspace)/$(artifactName)/*.vsix'
                    publisherId: '$(publisherId)'
                    extensionId: '$(extensionId)'
                    extensionName: '$(extensionName)'
                    updateTasksVersion: false
                    extensionVisibility: 'private'
                    extensionPricing: 'free'
Configure package.json for testing

Add test scripts to your package.json:

{
  "scripts": {
    "test": "mocha tests/_suite.js --reporter xunit --reporter-option output=test-results.xml",
    "test-verbose": "cross-env TASK_TEST_TRACE=1 npm test"
  }
}
Pipeline stage breakdown Stage 1: Test and validate Stage 2: Package extension Stage 3: Publish to marketplace Best practices for CI/CD

For classic build pipelines, follow these steps to set up extension packaging and publishing:

  1. Add the Bash task to compile the TypeScript into JavaScript.

  2. To query the existing version, add the Query Extension Version task using the following inputs:

  3. To package the extensions based on manifest Json, add the Package Extension task using the following inputs:

  4. To copy to published files, add the Copy files task using the following inputs:

  5. Add Publish build artifacts to publish the artifacts for use in other jobs or pipelines. Use the following inputs:

Stage 3: Download build artifacts and publish the extension
  1. To install the tfx-cli onto your build agent, add Use Node CLI for Azure DevOps (tfx-cli).

  2. To download the artifacts onto a new job, add the Download build artifacts task using the following inputs:

  3. To get the Publish Extension task, use the following inputs:

Optional: Install and test your extension

After you publish your extension, it needs to be installed in Azure DevOps organizations.

Install extension to organization

Install your shared extension in a few steps:

  1. Go to Organization settings and select Extensions.

  2. Locate your extension in the Extensions Shared With Me section:

  3. Check that the extension appears in your Installed extensions list:

Note

If you don't see the Extensions tab, ensure you're at the organization administration level (https://dev.azure.com/{organization}/_admin) and not at the project level.

End-to-end testing

After installation, perform comprehensive testing:

  1. Create a test pipeline:

  2. Validate functionality:

  3. Test performance:

Frequently asked questions Q: How is task cancellation handled?

A: The pipeline agent sends SIGINT and SIGTERM signals to task processes. While the task library doesn't provide explicit cancellation handling, your task can implement signal handlers. For details, see Agent jobs cancellation.

Q: How can I remove a task from my organization?

A: Automatic deletion isn't supported as it would break existing pipelines. Instead:

  1. Deprecate the task: Mark the task as deprecated
  2. Version management: Bump the task version
  3. Communication: Notify users about the deprecation timeline
Q: How can I upgrade my task to the latest Node.js version?

A: Upgrade to the latest Node version for better performance and security. For migration guidance, see Upgrading tasks to Node 20.

Support multiple Node versions by including multiple execution sections in task.json:

"execution": {
  "Node20_1": {
    "target": "index.js"
  },
  "Node10": {
    "target": "index.js"
  }
}

Agents with Node 20 use the preferred version, while older agents fall back to Node 10.

To upgrade your tasks:

Important

If you don't add support for the Node 20 runner to your custom tasks, they fail on agents installed from the pipelines-agent-* release feed.


RetroSearch is an open source project built by @garambo | Open a GitHub Issue

Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo

HTML: 3.2 | Encoding: UTF-8 | Version: 0.7.4