Создаём Azure DevOps extension

    У нас есть проект с настроенным CI/CD-процессом. Когда разработчик заканчивает задачу и вливает свои изменения в develop\qa, автоматически запускается билд, который выкладывает новую версию приложения на тестовую среду. В идеальном мире тестировщик автоматически узнаёт о задачах, которые были завершены, и на каком окружении они развёрнуты. В таком случае поток работ становится непрерывным, бесперебойным и требует меньше коммуникаций, отвлекающих от сосредоточенной работы. На практике всё не столь радужно.

    И вот однажды утром тимлид спросил меня: «А можешь сделать такую штуку для TFS, чтобы таскам, которые прикреплены к билду, после прохождения этого билда навешивался указанный тэг?»

    Я решил реализовать для задачи свой build\release task. Тем более что исходники всех build task’ов лежат на github, и вся информация доступна.

    Наш таск помечает цветом задачу, которая завершена, но не протестирована. Благодаря этому, разработчик сразу заметит, если забыл поставить нужный тег, а QA сразу увидит, что нужно проверить. Это визуализирует статус задач и ускоряет работу над проектом.

    В этой статье расскажу про реализацию необходимой логики, упаковку в extension. Так что если вам интересно, как создать такой плагин, добро пожаловать под кат.

    Для особо нетерпеливых: github и готовый extension на marketplace.



    В Azure DevOps есть возможность создавать фильтры, позволяющие покрасить таски на доске в разные цвета.




    Нас интересуют задачи, которые:

    • завершены
    • влиты на тестовую среду
    • ещё не проверены QA

    Для пунктов b и с больше всего подходят теги, но ставить их вручную противно, и все забывают это делать. Так что напишем extension, который после деплоя автоматически будет их проставлять.

    Таким образом, кастомный build\release step нам нужен для уменьшения человеческого фактора (разработчик забыл проставить тэг) и для помощи QA (сразу видно, что нужно проверять).

    Предварительные требования


    Для разработки extension'a нам понадобятся:

    1. любимая IDE
    2. установленный TypeScript+node.js+npm (сейчас у меня установлены версии 3.5.1\12.4\6.9.0)
    3. tfx-cli — библиотека для упаковки extension'a (npm i -g tfx-cli).

    Обратите внимание на наличие флага -g

    У Microsoft есть неплохая документация, в которой они как раз по верхам и рассказывают, как создать какой-нибудь extension. Кроме этого, точно так же есть и дока по созданию build\release task.

    На мой взгляд, в обеих статьях есть недостатки с точки зрения детализации или пояснения тех или моментов, поэтому я буду опираться на них, делая упор на тех моментах, которые в реальности показались мне не совсем очевидными.

    Вообще говоря, написать build\release step можно на достаточно большом количестве языков. Я приведу пример на TypeScript.

    А почему TypeScript?


    Самая первая версия build step’a была написана на PowerShell’e, про неё знала только наша команда и ещё парочка людей. Мы почти сразу столкнулись с тем, что если попробовать добавить таску к билду, который выполняется на docker build agent’e, то там не будет PowerShell’a и таска просто не отработает. Помимо этого, периодически у людей вылетали разного рода ошибки, которые были списаны на закидоны PowerShell’a. Отсюда вывод — решение должно быть кроссплатформенным.

    Структура проекта


    |--- README.md
    |--- images
        |---extension-icon.png
    |--- TaskFolder (тут будет вся логика нашего build\release step'a)
    |--- vss-extension.json (манифест файл)
    

    Далее нам нужно установить библиотеку для реализации build step'a

    1. cd TaskFolder
    2. npm init
    3. npm install azure-pipelines-task-lib --save && npm install @types/node --save-dev && npm install @types/q --save-dev
    4. tsc --init

    Разработка extension'a


    В первую очередь внутри TaskFolder нам необходимо создать файл task.json — это уже манифест-файл именно для самого build step'a. В нём содержится служебная информация (версия, создатель, описание), среда для запуска и конфигурация всех input'ов, которые мы в будущем увидим на форме.

    Более подробно изучить его структуру предлагаю в документации.
    В нашем случае на форме будет 2 input'a — тэг, который мы будем добавлять к work item'ам, и выбор типа pipeline (build или release).

    "inputs": [
            {
                "name": "pipelineType",
                "type": "pickList",
                "label": "Specify type of pipeline",
                "helpMarkDown": "Specify whether task is used for build or release",
                "required": true,
                "defaultValue": "Build",
                "options":{
                    "Build": "Build", 
                    "Release": "Release"
                }
            },
            {
                "name": "tagToAdd",
                "type": "string",
                "label": "Tag to add to work items",
                "defaultValue": "",
                "required": true,
                "helpMarkDown": "Specify a tag that will be added to work items"
            }
        ]
    

    По name далее в коде будем обращаться к значению каждого из input'ов.
    В TaskFolder создадим index.ts и напишем первый кусочек кода

    import * as tl from 'azure-pipelines-task-lib/task';
    
    async function run() {
        try {
            const pipelineType = tl.getInput('pipelineType');
        }
        catch (err) {
            tl.setResult(tl.TaskResult.Failed, err.message);
        }
    }
    
    run();
    

    Стоит отметить, что у TFS'a очень богатая документация по имеющемуся REST API, но на данный момент всё, что нам нужно, — достать work item'ы, привязанные к билду.

    Установим библиотеку для простого выполнения запросов

    npm install request --save && npm install request-promise-native --save
    

    Добавляем её в index.ts

    import * as request from "request-promise-native";
    

    Реализуем функцию, которая из текущего билда будем доставать привязанные work item'ы

    Немного про авторизацию


    Для доступа к REST API нам нужно получить accessToken

    const accessToken = tl.getEndpointAuthorization('SystemVssConnection', true).parameters.AccessToken;
    

    Далее нужно установить header authorization со значение “Bearer ${accessToken}”

    Возвращаемся к получению привязанных к билду work item’ов.

    Url Azure DevOps сервера и название TeamProject можно получить из environment variables следующим образом

    const collectionUrl = process.env["SYSTEM_TEAMFOUNDATIONCOLLECTIONURI"];
    const teamProject = process.env["SYSTEM_TEAMPROJECT"];
    

    async function getWorkItemsFromBuild() {
       const buildId = process.env["BUILD_BUILDID"];
       const uri = `${collectionUrl}/${teamProject}/_apis/build/builds/${buildId}/workitems`;
       const options = createGetRequestOptions(uri);
       const result = await request.get(options);
       return result.value;
    }
    

    function createGetRequestOptions(uri: string): any {
        let options = {
            uri: uri,
            headers: {
                "authorization": `Bearer ${accessToken}`,
                "content-type": "application/json"
            },
            json: true
        };
        return options;
    }
    

    В качестве ответа на GET запрос по URL

    ${collectionUrl}/${teamProject}/_apis/build/builds/${buildId}/workitems

    мы получаем вот такой JSON

    
    {
        "count": 3,
        "value": [
            {
                "id": "55402",
                "url": "https://.../_apis/wit/workItems/55402"
            },
            {
                "id": "59777",
                "url": "https://.../_apis/wit/workItems/59777"
            },
            {
                "id": "60199",
                "url": "https://.../_apis/wit/workItems/60199"
            }
        ]
    }
    

    По каждому из url через тот же самый REST API можно получить данные по work item'у.

    На данный момент наш метод run выглядит следующим образом.

    Метод для получения work item’ов из релиза почти идентичен уже описанному.

    async function run() {
       try {
           const pipelineType = tl.getInput('pipelineType');
           const workItemsData = pipelineType === "Build" ?
               await getWorkItemsFromBuild() :
               await getWorkItemsFromRelease();
           
       catch (err) {
           tl.setResult(tl.TaskResult.Failed, err.message);
       
    }
    

    Следующий шаг — для каждого из полученных work item'ов получить текущий набор тэгов и добавить указанный нами.

    Допишем метод run:

    async function run() {
        try {
            const pipelineType = tl.getInput('pipelineType');
            const workItemsData = pipelineType === "Build" ?
                await getWorkItemsFromBuild() :
                await getWorkItemsFromRelease();
            workItemsData.forEach(async (workItem: any) => {
                await addTagToWorkItem(workItem);
            });
        }
        catch (err) {
            tl.setResult(tl.TaskResult.Failed, err.message);
        }
    }

    Разберём добавление тэга к work item'ам

    Во-первых, нам нужно достать тэг, который мы указали на форме.

    const tagFromInput = tl.getInput('tagToAdd');
    

    Т.к. 2 шага назад мы получили url’ы до API каждого work item’a, то с их помощью мы можем легко запросить список текущих тэгов:

    const uri = workItem.url + "?fields=System.Tags&api-version=2.0";
    const getOptions = createGetRequestOptions(uri)
    const result = await request.get(getOptions);
    

    В ответ получаем вот такой JSON:

    {
        "id": 55402,
        "rev": 85,
        "fields": {
            "System.Tags": "added-to-prod-package; test-tag"
        },
        "_links": {
            "self": {
                "href": "https://.../_apis/wit/workItems/55402"
            },
            "workItemUpdates": {
                "href": "https://.../_apis/wit/workItems/55402/updates"
            },
            "workItemRevisions": {
                "href": "https://.../_apis/wit/workItems/55402/revisions"
            },
            "workItemHistory": {
                "href": "https://.../_apis/wit/workItems/55402/history"
            },
            "html": {
                "href": "https://..../web/wi.aspx?pcguid=e3c978d9-6ea1-406f-987d-5b03e24973a1&id=55402"
            },
            "workItemType": {
                "href": "https://.../602fd27d-4e0d-4aec-82a0-dcf55c8eef73/_apis/wit/workItemTypes"
            },
            "fields": {
                "href": "https://.../_apis/wit/fields"
            }
        },
        "url": "https://.../_apis/wit/workItems/55402"
    }
    

    Берём все старые тэги и добавляем к ним новый:

    const currentTags = result.fields['System.Tags'];
        let newTags = '';
        if (currentTags !== undefined) {
            newTags = currentTags + ";" + tagFromInput;
        } else {
            newTags = tagFromInput;
        }

    Отправляем patch запрос к work item api:

    const patchOptions = getPatchRequestOptions(uri, newTags);
    await request.patch(patchOptions)
    
    function getPatchRequestOptions(uri: string, newTags: string): any {
        const options = {
            uri: uri,
            headers: {
                "authorization": `Bearer ${accessToken}`,
                "content-type": "application/json-patch+json"
            },
            body: [{
                "op": "add",
                "path": "/fields/System.Tags",
                "value": newTags
            }],
            json: true
        };
        return options
    }
    

    Сборка и упаковка extension’a


    Для красоты всего происходящего предлагаю в tsconfig.json в compilerOptions добавить
    "outDir": "dist"
    . Теперь, если мы выполним команду tsc внутри TaskFolder, то получим папку dist, внутри которой будет index.js, который далее и пойдёт в финальный пакет.

    Т.к. наш index.js находится в папке dist и далее мы так же её и скопируем в итоговый пакет, необходимо немного поправить task.json:

    "execution": {
            "Node": {
                "target": "dist/index.js"
            }
        }

    В vss-extension.json в секции files необходимо явно объявить, что будет скопировано в итоговый пакет.

    "files": [
        {
          "path": "TaskFolder/dist",
          "packagePath": "TaskFolder/dist"
        },
        {
          "path": "TaskFolder/node_modules",
          "packagePath": "TaskFolder/node_modules"
        },
        {
          "path": "TaskFolder/icon.png",
          "packagePath": "TaskFolder/icon.png"
        },
        {
          "path": "TaskFolder/task.json",
          "packagePath": "TaskFolder/task.json"
        }
      ]
    

    Последний шаг — нам нужно запаковать наше расширение.

    Для этого выполняем команду:

     tfx extension create --manifest-globs ./vss-extension.json

    После выполнения получаем *.vsix файл, который далее и будет устанавливаться в TFS.

    P.S. *.vsix файл — по своей сути обычный архив, его спокойно можно открыть через 7-zip, например, и посмотреть, что внутрь действительно попало всё, что нужно.

    Добавим немного красоты


    Если вы хотите, чтобы при выборе вашего build step'a в процессе добавления его к pipeline у него было изображение, то этот файл нужно поместить рядом с task.json и назвать icon.png. В сам task.json никаких изменений вносить не нужно.

    В vss-extension.json можно добавить секцию:

    "icons": {
        "default": "images/logo.png"
      }

    Это изображение будет отображаться в галерее локальных расширений.

    Установка extension'a


    1. Переходим по адресу tfs_server_url/_gallery/manage
    2. Нажимаем Upload extension
    3. Указываем путь или drag'n'drop'ом перекидываем полученный ранее *.vsix файл
    4. После того, как пройдёт верификация, в контекстном меню расширения выбираем view extension, на открывшейся странице выбираем коллекцию, в которую его нужно установить
    5. После этого extension'ом можно начинать пользоваться.

    Использование build step'a


    1. Открываем нужный вам pipeline
    2. Идём в добавление build step'a
    3. Ищем extension


    4. Указываем все необходимые настройки

    5. Радуемся жизни :)

    Заключение


    В этот статье я показал, как сделать плагин для Azure DevOps, который автоматически проставляет нужный тег к задачам для релиза. Коллеги встроили его в pipeline, которые проходят как на windows, так и на linux билд-агентах.

    Благодаря этому плагину, нам стало легче работать с задачами и выстроить непрерывную работу над проектом. Разработчики теперь не отвлекаются на посторонние вещи, а QA оперативно узнает о новых задачах на тестирование.

    Ещё раз напомню ссылку на скачивание: ссылка

    Будем рады обратной связи и пожеланиям к доработке :)

    P.S.

    Также есть идея добавить в плагин возможность убирать указанные тэги. Если тестировщик нашёл багу и приходится деплоить таску ещё раз, можно было бы избавиться от тэгов “Verified“).
    True Engineering
    95.98
    Лаборатория технологических инноваций
    Share post

    Comments 1

      0
      Есть ли среди читателей те, кто написал расширение для DevOps на платной основе? :)

      Only users with full accounts can post comments. Log in, please.