YouTrack과 Azure DevOps를 통합하는 방법

우리는 오랫동안 버전 제어를 위해 Azure DevOps를 사용하고 있습니다. 1년 전까지 우리는 프로젝트 추적에도 사용했으며 보드와 Repos 간의 통합은 원활했습니다. 새로운 프로젝트 추적 도구는 Azure DevOps와의 통합이 없는 Jetbrains YouTrack입니다.

그러나 YouTrack에 대한 경험이 있다면 엄청난 잠재력을 가지고 있고 확장 지점 역할을 할 수 있는 워크플로우의 개념에 익숙해야 합니다. 스크립트는 Javascript로 작성되며 일정에 따라 트리거를 기반으로 실행할 수 있습니다.

우리의 아이디어는 이슈가 진행 중이 되면 이슈 ID 이름으로 기능 분기를 자동으로 생성하고 이슈가 풀 요청 상태에 있으면 풀 요청을 생성하는 것이었습니다. 분기 및 풀 요청에 대한 링크가 이슈에 필드로 추가됩니다.

먼저 여러 스크립트 간에 공유되는 리소스에 대한 공통 파일을 만들었습니다.

var http = require('@jetbrains/youtrack-scripting-api/http');

function issueUrl(id) {
  return "https://youryoutrackinstanceurl/issue/" + id;
}

class Azure {
  constructor() {        
    this.organization = "your-azure-devops-organization";
    this.project = "Name of the Azure DevOps project";
    this.repository = "Name of the repository";
    this.defaultReviewers = [{
      "id": "ccd7d2cf-7120-4655-836e-a3ae28256dbd",
    }];
    this.login = "[email protected]";
    this.PAT = "fdewqk321n55l55x7qmsfnlgdpbqnyhvdakdfsnczpwbriqjbhxvq";

  }

  url(api, item, operation, api_version) {
    var link = "https://dev.azure.com/" + this.organization + "/" + this.project + "/_apis/" + api;
    if (item)
      link += "/" + item;
    if (operation)
      link += "/" + operation;
    if (api_version)
      link += (link.includes("?") ? "&" : "?") + "api-version=" + api_version;
    return link;
  }

  branchUrl(branchName) {
    return "https://dev.azure.com/" + this.organization + "/" + this.project + "/_git/" + this.repository + "?version=GB" + branchName;
  }

  prUrl(id) {
    return "https://dev.azure.com/" + this.organization + "/" + this.project + "/_git/" + this.repository + "/pullrequest/" + id;
  }

  connection() {
    var connection = new http.Connection("", null, 2000);
    connection.basicAuth(this.login, this.PAT);
    return connection;
  }

  getFrom(url) {
    var connection = this.connection();
    var resp = connection.getSync(url);
    if (!resp.isSuccess) {
      throw new Error("" + resp.code);
    }    
    return JSON.parse(resp.response);
  }

  sendTo(url, data, mimeType = "application/json", action = "post") {
    var connection = this.connection();
    connection.addHeader({
      name: "Content-Type",
      value: mimeType
    });
    var resp = action == "patch" ? connection.patchSync(url, null, data) : connection.postSync(url, null, data);
    if (!resp.isSuccess) {
      throw new Error("" + resp.code);
    }
    return JSON.parse(resp.response);
  }  
}

module.exports.Azure = Azure;
module.exports.issueUrl = issueUrl;


Azure의 생성자에서 projectName 매개 변수를 전달하면 보다 유연한 솔루션을 얻을 수 있지만 간단하게 하기 위해 샘플에 추가하지 않았습니다. 그리고 같은 이유로 이 예에서 PAT 인증을 사용했습니다.

다음으로 문제에 대한 작업이 시작되면 분기를 생성하는 스크립트를 추가했습니다. 우리의 경우 진행 중 상태로 푸시하여 표시됩니다.

var common = require('./common');
var entities = require('@jetbrains/youtrack-scripting-api/entities');
var workflow = require('@jetbrains/youtrack-scripting-api/workflow');

exports.rule = entities.Issue.onChange({
  title: "Create branch for issue",
  guard: function(ctx) {
    return ctx.issue.becomes(ctx.State, ctx.State.InProgress);
  },
  action: function(ctx) {
    logger.log("Creating branch for issue " + ctx.issue.id);

    try {

      const azure = new common.Azure();

      var issueBranchName = ctx.issue.id;

      var issueBranch = azure.getFrom(azure.url("git/repositories", azure.repository, "refs?filter=heads/&filterContains=" + issueBranchName, "6.0"));
      if (issueBranch && issueBranch.count && (issueBranch.value[0].name == "refs/heads/" + issueBranchName)) {        
        ctx.issue.fields.Branch = azure.branchUrl(issueBranchName);
        return;
      }

      var parentBranchName = "main";

      var repository = azure.getFrom(azure.url("git/repositories", azure.repository, null, "6.0"));
      var createBranchUrl = azure.url("git/repositories", repository.id, "refs", "6.0");
      var parentBranch = azure.getFrom(azure.url("git/repositories", azure.repository, "refs?filter=heads/&filterContains=" + parentBranchName, "6.0"));
      if (!parentBranch.count) {        
        workflow.message(workflow.i18n('Branch ' + parentBranchName + " not found in repository"));
        return;
      }
      var parentObjectId = parentBranch.value[0].objectId;

      var data = [{
        "name": "refs/heads/" + issueBranchName,
        "oldObjectId": "0000000000000000000000000000000000000000",
        "newObjectId": parentObjectId
      }];

      var response = azure.sendTo(createBranchUrl, JSON.stringify(data));
      if (!response || (response.count != 1) || !response.value[0].success) {
        workflow.message(workflow.i18n('Failed to create Azure DevOps branch ') + issueBranchName);
        workflow.message(response);
        return;
      }

      workflow.message(workflow.i18n('Created Azure DevOps branch ') + issueBranchName);

      ctx.issue.fields.Branch = azure.branchUrl(issueBranchName);

    } catch (e) {
      workflow.message(workflow.i18n('Failed to access Azure DevOps') + "\n" + e.message);
    }
  },
  requirements: {
    State: {
      name: "State",
      type: entities.State.fieldType,
      InProgress: {
        name: "In Progress"
      }
    },
    BranchField: {
      name: "Branch",
      type: entities.Field.stringType
    }
  }
});


마지막 단계는 양수인이 문제의 상태를 풀 요청으로 설정하면 풀 요청을 생성하는 것입니다.

var common = require('./common');
var entities = require('@jetbrains/youtrack-scripting-api/entities');
var workflow = require('@jetbrains/youtrack-scripting-api/workflow');

exports.rule = entities.Issue.onChange({
  title: "Create PR for issue",
  guard: function(ctx) {
    return ctx.issue.becomes(ctx.State, ctx.StateState.PullRequest);
  },
  action: function(ctx) {

    try {

      const azure = new common.Azure();

      var issueBranchName = ctx.issue.id;

      var issueBranch = azure.getFrom(azure.url("git/repositories", azure.repository, "refs?filter=heads/&filterContains=" + issueBranchName, "6.0"));
      if (!issueBranch || !issueBranch.count || (issueBranch.value[0].name != "refs/heads/" + issueBranchName)) {        
        workflow.message(workflow.i18n('Issue branch ' + issueBranchName + ' not found'));
        return;
      }

      var parentBranchName = "main";

      var repository = azure.getFrom(azure.url("git/repositories", azure.repository, null, "6.0"));

      var existingPr = azure.getFrom(azure.url("git/repositories", repository.id, "pullrequests?searchCriteria.sourceRefName=refs/heads/" + issueBranchName + "&searchCriteria.status=active&searchCriteria.targetRefName=refs/heads/" + parentBranchName, "6.0"));
      if (existingPr && existingPr.count && existingPr.value[0]) {
        var prId = existingPr.value[0].pullRequestId;
        workflow.message(workflow.i18n('PR ' + prId + ' already active'));
        ctx.issue.fields.PR = azure.prUrl(prId);
        return;
      }

      var createPrUrl = azure.url("git/repositories", repository.id, "pullrequests", "6.0");

      var data = {
        "sourceRefName": "refs/heads/" + issueBranchName,
        "targetRefName": "refs/heads/" + parentBranchName,
        "title": ctx.issue.id + " " + ctx.issue.summary,
        "description": common.issueUrl(ctx.issue.id),
        "reviewers": azure.defaultReviewers
      };

      var response = azure.sendTo(createPrUrl, JSON.stringify(data));
      if (!response || !response.pullRequestId) {
        workflow.message(workflow.i18n('Failed to create PR for branch ') + issueBranchName);
        return;
      }


      var prId = r.pullRequestId;
      workflow.message(workflow.i18n('Created PR ') + prId);
      ctx.issue.fields.PR = azure.prUrl(prId);

    } catch (e) {
      workflow.message(workflow.i18n('Failed to access Azure DevOps') + "\n" + e.message);
    }
  },
  requirements: {
    State: {
      name: "State",
      type: entities.State.fieldType,
      PullRequest: {
        name: "Pull request"
      },
    }
  }
});


그게 다야! 로깅을 추가하는 것이 좋습니다. 모든 티켓이 통합 워크플로를 트리거하지 않도록 티켓 유형에 제약 조건을 추가할 수 있습니다.

가능성은 무한합니다.

좋은 웹페이지 즐겨찾기