Debugging a Azure DevOps Task Extension (TypeScript) - azure-devops

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
I follow the tutorial create a custom pipelines task and build a sample app with it
I clone the ansible task extension and checkout the programming style
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 ;.

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
From the result, you can see the two parameters can be passed normally.

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);
}
}

Related

error on load RestClient on Azure devops extension dev

Im new in extensión dev for #azure #DevOps, I already publish one, the extensión by now can save and get documents via
VSS.getService(VSS.ServiceIds.ExtensionData).then(function (
dataService: any
) {...});
I want to get all teams from the project,and I have 2 problems, First I try to import the Rest client like this:
import RestClient = require("TFS/WorkItemTracking/RestClient");
But, When I upload to azure Devops and display the page in the consle say:
index.js:1 Uncaught ReferenceError: define is not defined
at index.js:1:1
Second I dont relly know how to use the getTeams function, I try this, but I can not test it, If u could give me an example would be great
var client = RestClient.getClient();
client.getTeams(VSS.getWebContext().project.id).then(function(getTeams){console.log(getTeams)})
I try another API and it works like this:
VSS.require(["VSS/Service", "TFS/WorkItemTracking/RestClient"], function (VSS_Service:any, TFS_Wit_WebApi:any) {
// Get the REST client
var witClient = VSS_Service.getCollectionClient(TFS_Wit_WebApi.WorkItemTrackingHttpClient);
// ...
console.log("obtiene cliente")
witClient.getWorkItems(/* some work item IDs */ [1,2,3,4], ["System.Title"]).then(
function(workItems:any) {
console.log(JSON.stringify(workItems));
});
});
But, It doesnt work when I try my first aproach, like this:
import RestClient = require("TFS/WorkItemTracking/RestClient");
I try to check my config files but dont know what to do, I have this in my package.json
"dependencies": {
"vss-web-extension-sdk": "^5.141.0"
},
and in my file tsconfig.json
{
"compilerOptions": {
"target": "es5",
"lib": ["es5",
"es6",
"dom"],
"module": "amd",
"types": ["vss-web-extension-sdk"],
"moduleResolution": "node",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true
}

ERR_REQUIRE_ESM require of of ES Module not supported how can I fix this? on file-type package

I've a outdated app that uses very older few packages those doesn't support ES Module as an example file-type package. So if you setup babel and node HTTP server with and then install file-type package then start building and running will throw error message like below:
Error [ERR_REQUIRE_ESM]: require() of ES Module E:\test\testbabel\node_modules\file-
type\index.js from E:\test\testbabel\dist\index.js not supported.
Instead change the require of E:\test\testbabel\node_modules\file-type\index.js in
E:\test\testbabel\dist\index.js to a dynamic import() which is available in all CommonJS
modules.
at Object.<anonymous> (E:\test\testbabel\dist\index.js:10:17) {
code: 'ERR_REQUIRE_ESM'
}
I tried this on a fresh project though my old project has an outdated config or so, It still throwing this error
Here are my index.js codes
import http from 'http';
import { fileTypeFromFile } from 'file-type';
const server = http.createServer((req, res) => {
res.end('Hello from the server');
}).listen(4001);
console.log('Server is up and running');
export default server;
file package.json.
{
"name": "testbabel",
"version": "1.0.0",
"description": "test babel with http or express",
"main": "index.js",
"scripts": {
"build": "babel index.js -d dist",
"start": "npm run build && node dist/index.js"
},
"author": "",
"license": "ISC",
"devDependencies": {
"#babel/cli": "^7.17.10",
"#babel/core": "^7.18.2",
"#babel/plugin-transform-modules-commonjs": "^7.18.2",
"#babel/preset-env": "^7.18.2"
},
"dependencies": {
"file-type": "^17.1.1"
}
}
I just tried to import the package and got the errors above.
attempt:
I thought a converter might help so used #babel/plugin-transform-modules-commonjs but still didn't help, and seems no effect on including that package
I'm not sure but added some tweaks on package.json like "type": "module" "type": "commonjs" didn't help at all.
what is the easiest solution for this issue and how do we fix it?
Note: I saw people were going back to the supported package instead of new one which doesn't make sense to me as a solution.
Option1(babel with mocha): Rename "index.js" to "index.mjs" and modify file-type's pacakage.json ("index.js" to "index.mjs"), then leave Babel to transpile for you.
// babel-register.js
const babel_register = require("#babel/register").default;
babel_register({
ignore: [
// Only work on Project-wide configuration
// overrides ignore can transpile packages(modules) from node_modules (https://babeljs.io/docs/en/babel-register/#ignores-node_modules-by-default)
],
});
Use babel.config instead of .babelrc
//.mocharc.js
require("./babel-register");
module.exports = {
// https://github.com/mochajs/mocha/blob/v8.4.0/example/config/.mocharc.js
ui: "bdd",
timeout: 5000,
recursive: true,
};
Option2(babel only): Using dynamic import expression
async function doSomething() {
const {fileTypeFromStream} = await import("file-type");
}
and
["#babel/preset-env", {
exclude: ["proposal-dynamic-import"]
}]
Avoiding Babel tanspile dynamic import expression

Cannot test a native Android app using codeceptJS

I have created a codeceptJS project by following the mobile testing setup steps located here: https://codecept.io/mobile/#setting-up
So far, I'm unable to test any apps via simulator; I instead get the following error:
1) login
I should be able to login with the correct username and password:
>> The requested resource could not be found, or a request was received using an HTTP method that is not supported by the mapped resource <<
at Object.getErrorFromResponseBody (node_modules/webdriver/build/utils.js:189:12)
at NodeJSRequest._request (node_modules/webdriver/build/request/index.js:157:31)
at processTicksAndRejections (internal/process/task_queues.js:93:5)
I have verified my appium config using appium-doctor, and there are no issues found.
My codecept.conf.js is as follows:
const path = require('path');
const { setHeadlessWhen } = require('#codeceptjs/configure');
// turn on headless mode when running with HEADLESS=true environment variable
// export HEADLESS=true && npx codeceptjs run
setHeadlessWhen(process.env.HEADLESS);
exports.config = {
tests: './*_test.js',
output: './output',
helpers: {
Appium: {
platform: 'Android',
device: 'emulator',
desiredCapabilities: {
avd: 'Pixel_5_API_28',
app: path.resolve('./sample_apps/Android.apk'),
appActivity: 'com.swaglabsmobileapp.MainActivity'
}
},
},
include: {
I: './steps_file.js'
},
bootstrap: null,
mocha: {},
name: 'appium-codecept-android-POC',
plugins: {
pauseOnFail: {},
retryFailedStep: {
enabled: true
},
tryTo: {
enabled: true
},
screenshotOnFail: {
enabled: true
}
}
}
And here's my package.json as created by codeceptjs init:
{
"name": "appium-codecept-android-POC",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"codeceptjs": "^3.0.7",
"webdriverio": "^7.9.0"
}
}
Finally, my test file is as follows:
Feature('login');
Scenario('I should be able to login with the correct username and password', ({ I }) => {
setTimeout(() => {
I.see('Username');
}, 3000);
I.click('//android.widget.EditText[#content-desc="test-Username"]');
I.fillField('//android.widget.EditText[#content-desc="test-Username"]', 'standard_user');
I.click('//android.widget.EditText[#content-desc="test-Password"]');
I.fillField('//android.widget.EditText[#content-desc="test-Password"]', 'secret_sauce');
I.click('//android.view.ViewGroup[#content-desc="test-LOGIN"]');
I.waitForElement('//android.view.ViewGroup[#content-desc="test-Cart drop zone"]/android.view.ViewGroup/android.widget.TextView', 3)
I.dontSeeElement('//android.view.ViewGroup[#content-desc="test-Error message"]/android.widget.TextView');
});
Scenario('I should not be able to login with an incorrect username or password', ({ I }) => {
setTimeout(() => {
I.see('Username');
}, 3000);
I.click('//android.widget.EditText[#content-desc="test-Username"]');
I.fillField('//android.widget.EditText[#content-desc="test-Username"]', 'bob');
I.click('//android.widget.EditText[#content-desc="test-Password"]');
I.fillField('//android.widget.EditText[#content-desc="test-Password"]', 'secret_sauce');
I.click('//android.view.ViewGroup[#content-desc="test-LOGIN"]');
I.waitForElement('//android.view.ViewGroup[#content-desc="test-Cart drop zone"]/android.view.ViewGroup/android.widget.TextView', 3)
I.dontSeeElement('//android.view.ViewGroup[#content-desc="test-Error message"]/android.widget.TextView');
});
Scenario('I should be able to see details', ({ I }) => {
// login
setTimeout(() => {
I.see('Username');
}, 3000);
I.click('//android.widget.EditText[#content-desc="test-Username"]');
I.fillField('//android.widget.EditText[#content-desc="test-Username"]', 'standard_user');
I.click('//android.widget.EditText[#content-desc="test-Password"]');
I.fillField('//android.widget.EditText[#content-desc="test-Password"]', 'secret_sauce');
I.click('//android.view.ViewGroup[#content-desc="test-LOGIN"]');
I.waitForElement('//android.view.ViewGroup[#content-desc="test-Cart drop zone"]/android.view.ViewGroup/android.widget.TextView', 3)
// should be able to click a label to see details
I.click('(//android.widget.TextView[#content-desc="test-Item title"])[2]');
I.seeElement('//android.view.ViewGroup[#content-desc="test-Description"]/android.widget.TextView[2]');
I.click('//android.view.ViewGroup[#content-desc="test-BACK TO PRODUCTS"]');
});
I'm at a loss here, as I haven't done anything except follow the setup instructions. Executing against ios works; it is only the android execution that fails. Appium is installed and running, env vars are set, etc. Any help would be appreciated, as this could be a deal breaker for me in terms of whether or not I can use codeceptjs. I love the project and really want to use it, but I must be able to test both ios and android native apps.
One final note: If anyone wants to try this config, the app I am using for the above test can be found here: https://github.com/saucelabs/sample-app-mobile/releases/download/2.7.1/Android.SauceLabs.Mobile.Sample.app.2.7.1.apk

Gatsby trailing slash redirect issue only on deployed version

In my Gatsby app, I want the following routes:
/branches - shows stores' branch locations.
/branches/:id - shows information about a particular branch.
To accomplish this, I have the following folder structure:
src
pages
branches
index.tsx
Inside index.tsx, I have:
import React from 'react'
import { Router } from '#reach/router'
const Comp = (props: { path: any }) => {
return <pre>{JSON.stringify(props, null, 2)}</pre>
}
export default () => {
return (
<Router>
<Comp path="/branches" />
<Comp path="/branches/:id" />
</Router>
)
}
(The Comp component is there just to show the functionality is working.)
In my gatsby-node.js file, I have this:
// Implement the Gatsby API “onCreatePage”. This is
// called after every page is created.
exports.onCreatePage = async ({ page, actions }) => {
const { createPage } = actions
// page.matchPath is a special key that's used for matching pages
// only on the client.
if (page.path.match(/^\/branches/)) {
page.matchPath = '/branches/*'
// Update the page.
createPage(page)
}
}
So the result is that when I am running gatsby develop, everything works as expected. I can visit /branches and /branches/4 and I see what I expect, the Comp component displaying some routing information:
{
"path": "/branches",
"uri": "/branches",
"location": {
"pathname": "/branches/",
"search": "",
"hash": "",
"href": "http://localhost:1337/branches/",
"origin": "http://localhost:1337",
"protocol": "http:",
"host": "localhost:1337",
"hostname": "localhost",
"port": "1337",
"state": null,
"key": "initial"
}
}
However, when running deploying to S3 using gatsby build && gatsby-plugin-s3 deploy --yes, I get into a redirect loop:
What is the issue here and how can I resolve it?
(I have experimented with the gatsby-plugin-force-trailing-slashes and gatsby-plugin-remove-trailing-slashes plugins to no avail)

How to resolve variables inside vscode extension

I am creating an extension that will expose a command to be executed through tasks executors from vscode and I would like to use the native resolution for variables.
For example:
In my tasks configuration I want to use it like this:
"inputs": [
{
"id": "specInput",
"type": "command",
"command": "shellCommand.execute",
"args": {
"command": "find cypress/integration -type f",
"cwd": "${workspaceFolder}/e2e/"
}
}
]
Relevant code:
const commandHandler = (args: ShellCommandOptions) => {
const workspaceFolder = vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders[0].uri.fsPath;
const cwd = args.cwd || workspaceFolder;
}
Full extension code can be found here.
The problem is, when the cwd parameter arrives at my extension, the workspaceFolder is not resolved. How could I resolve it? I didn't want to re-implement vscode's features.