3
votes

I develop all our task extensions in PowerShell, now I start to translate my first extension into TypeScript. The extension is a small task which should run in build or release pipelines. The task should get deployed to a Azure DevOps Server 2020.1 (on prem).


Preparation

Tutorials

System Setup

- Visual Studio Code
- Node (v14.15.4)
- TypeScript (Version 4.1.3)
- ts-node (v9.1.1)
- mocha (8.2.0)
- ts-mocha (8.0.0)
- azure-pipelines-task-lib (2.12.0)

Launch.json

{
  "version": "0.2.0",
  "configurations": [
    {
      "args": ["task/index.ts", "--Template", "Custom"],
      "internalConsoleOptions": "openOnSessionStart",
      "name": "Run TypeScript",
      "request": "launch",
      "runtimeArgs": ["--nolazy", "-r", "ts-node/register/transpile-only"],
      "skipFiles": ["<node_internals>/**"],
      "type": "pwa-node"
    }
  ]
}

Start up command: node.exe --nolazy -r ts-node/register/transpile-only task/index.ts --Template Custom


The Issue

At runtime, when the tl.getInput function with required true get executed, the debugging stop immediately without any response (no error, no output).

App.ts:

import tl = require("azure-pipelines-task-lib/task");
export const App = {
  Param: {
      Test: "Here",
      Template: tl.getInput("Template", true),
  }
}

Index.ts (entry point):

import { App } from "./app";

function run() {
  console.log("Hello");
  console.log(App.Param.Test);
}

run();

Output (just nothing):

Index.ts (modified):

import { App } from "./app";

function run() {
  console.log("Hello");
  // console.log(App.Param.Test);
}

run();

Output (modified):

Hello

obviously it stops because the required variable Template get not passed to the application.


The Question

  • is there a way to debug an azure devops task extension?
  • is it possible to pass parameter and load them via tl.getInput?
  • is there a state of the art or a complete guideline how to develop azure devops task extension?

It is totally clear that running azure-pipelines-task-lib without a Azure DevOps environment run into issues. But I was hoping that it is possible to mockup the required pipeline variables and run this library locally. If using azure-pipelines-task-lib means that you have to deploy the extension and run it in a pipeline to test, it get kind of komplex to develop tasks with it, or?


Edit 1:

I found the deprecated repository about vsts-task-lib. In azure-pipelines-tasks/docs/debugging.md is manual to debug that library. The author of Debugging TypeScript Tasks in VS Code describe an example launch.json configuration and I modify it for my usecase:

{
      "name": "Launch tar.gz",
      "type": "node",
      "request": "launch",
      "program": "${workspaceFolder}/dist/task/index.js",
      "stopOnEntry": false,
      "args": [],
      "cwd": "${workspaceRoot}/task",
      "preLaunchTask": "tsc: build - tsconfig.json",
      "runtimeExecutable": null,
      "runtimeArgs": ["--nolazy"],
      "env": {
        "NODE_ENV": "development",
        "INPUT_Separator": ";",
        "BUILD_SOURCESDIRECTORY": "C:\\agents\\latest\\_work\\21\\s"
      },
      "sourceMaps": true,
      "outFiles": ["${workspaceRoot}/dist"]
    }

I can confirm that it is possible to start up debugging and the tl.getInput("Separator") will return ;.

2

2 Answers

0
votes

is there a way to debug an azure devops task extension?

Yes, According to the Step 1 in the article "Add a custom pipelines task extension", after installing all the required libraries and dependencies and adding all the required task implementation files, you can compile and run the task with PowerShell or other shells. By default, the task is run with debugging mode. See the example I share below.

is it possible to pass parameter and load them via tl.getInput?

Sure, you can pass the value of tl.getInput as an parameter. See the example I share below.

is there a state of the art or a complete guideline how to develop azure devops task extension?

Currently, the Microsoft Docs about DevOps extensions is the best guide for us to develop DevOps extensions.

Follow your case, I also test on my side, below are the main source code I use:

  • task.json
{
    "$schema": "https://raw.githubusercontent.com/Microsoft/azure-pipelines-task-lib/master/tasks.schema.json",
    "id": "dc7322d8-6c98-4be7-91c9-dcbf7f4df7dd",
    "name": "buildAndReleaseTask",
    "friendlyName": "Build and Release task",
    "description": "Test create a Build and Release task.",
    "helpMarkDown": "",
    "category": "Utility",
    "author": "Bright Ran",
    "version": {
        "Major": 0,
        "Minor": 1,
        "Patch": 0
    },
    "instanceNameFormat": "Echo $(UserName)",
    "inputs": [
        {
            "name": "UserName",
            "type": "string",
            "label": "User name",
            "defaultValue": "",
            "required": true,
            "helpMarkDown": "An user name"
        }
    ],
    "execution": {
        "Node10": {
            "target": "index.js"
        }
    }
}
  • App.ts (almost same as yours)
import tl = require("azure-pipelines-task-lib/task");
export const App = {
  Param: {
      Here: "Here",
      UserName: tl.getInput("UserName", true),
  }
}
  • index.ts (almost same as yours)
import { App } from "./App";

function run() {
  console.log("Hello,", App.Param.UserName);
  console.log("Look", App.Param.Here);
}

run();
  • Result to compile and run the task.
tsc
$env:INPUT_USERNAME="xxxx"
node index.js

enter image description here

From the result, you can see the two parameters can be passed normally.

0
votes

With the help of Debugging TypeScript Tasks in VS Code I was able to do the following things:

  • read input parameter with tl.getInput from import tl = require("azure-pipelines-task-lib/task")
  • read environment variable with tl.getVariable from import tl = require("azure-pipelines-task-lib/task")
  • connect to Azure DevOps Server with new azdev.WebApi from import * as azdev from "azure-devops-node-api"
  • make a build api request with getBuildApi from import * as ba from "azure-devops-node-api/BuildApi"
  • run and debug application directly with TypeScript without JavaScript translation

launch.json

{
  "name": "Run TypeScript",
  "type": "pwa-node",
  "request": "launch",
  "internalConsoleOptions": "openOnSessionStart",
  "stopOnEntry": false,
  // path to your ts file
  "args": ["index.ts"],
  "cwd": "${workspaceRoot}/task",
  "runtimeArgs": ["--nolazy", "-r", "ts-node/register/transpile-only"],
  "env": {
    "NODE_ENV": "development",
    // param (enter your input params here!)
    "INPUT_WebhookUrl": "MyVariables",
    "INPUT_Template": "Empty",
    "INPUT_Json": "{\"text\":\"I am a test message\",\"attachments\":[{\"text\":\"And here’s an attachment!\"}]}",
    "INPUT_Separator": ";",
    // env
    "AGENT_JOBSTATUS": "Succeeded",
    "AGENT_NAME": "MyAgent",
    "BUILD_BUILDID": "5",
    "BUILD_BUILDNUMBER": "20210108.1",
    "BUILD_REASON": "Scheduled",
    "BUILD_REPOSITORY_NAME": "MyRepo",
    "BUILD_SOURCEBRANCHNAME": "master",
    "BUILD_SOURCEVERSION": "122a24f",
    "BUILDCONFIGURATION": "Debug",
    "BUILDPLATFORM": "Any CPU",
    "SYSTEM_ACCESSTOKEN": "",
    "SYSTEM_DEFINITIONNAME": "MyDefinitionName",
    "SYSTEM_TEAMFOUNDATIONSERVERURI": "https://myurl.de/mycollection/",
    "SYSTEM_TEAMPROJECT": "PSItraffic",
    // debug
    "DEBUG_PAT": "my debug pat"
  },
  "skipFiles": ["<node_internals>/**"]
}

Usecases

Read param & env: app.ts

import tl = require("azure-pipelines-task-lib/task");

export const App = {
  // ------------------------------------------------------------ param
  Param: {
    WebhookUrl: tl.getDelimitedInput("WebhookUrl", "\n", true),
    Template: tl.getInput("Template", true)
  },
  // ------------------------------------------------------------ env
  Env: {
    Agent: {
      Jobstatus: getVariable("AGENT_JOBSTATUS"),
      Name: getVariable("AGENT_NAME"),
    },

    ...

    System: {
      AccessToken: getVariable("SYSTEM_ACCESSTOKEN"),
      DefinitionName: getVariable("SYSTEM_DEFINITIONNAME"),
      TeamFoundationServerUri: getVariable("SYSTEM_TEAMFOUNDATIONSERVERURI"),
      TeamProject: getVariable("SYSTEM_TEAMPROJECT"),
    },
  // ------------------------------------------------------------ debug
  Debug: {
    Pat: getVariable("DEBUG_PAT"),
  },
}

function getVariable(name: string): string {
  // get variable
  let v = tl.getVariable(name);
  if (v === undefined) return "";
  return v;
}

connect to azure devops server: rest.ts

import { App } from "./app";
import * as azdev from "azure-devops-node-api";
import * as ba from "azure-devops-node-api/BuildApi";


export class Rest {
  static AuthHanlder: IRequestHandler = Rest.Auth();
  static Connection: azdev.WebApi = new azdev.WebApi(App.Env.System.TeamFoundationServerUri, Rest.AuthHanlder);

  static Auth(): IRequestHandler {
    // auth
    if (App.Env.System.AccessToken === "") return azdev.getPersonalAccessTokenHandler(App.Debug.Pat);
    // no sure if this works on production
    return azdev.getBearerHandler(App.Env.System.AccessToken);
  }
}