YouTrack は、ワークフロー API への REST クライアントの実装をサポートします。ワークフローを使用して、お気に入りのツールとのプッシュスタイルの統合をスクリプト化できます。
// Post issue content to a third-party tool and add the returned response as a comment
const connection = new http.Connection('https://server.com');
connection.addHeader('Content-Type', 'text/html');
const response = connection.postSync('/issueRegistry', [], issue.description);
if (response && response.code === 200) {
issue.addComment(response.response);
}
認証
REST クライアントは、ヘッダーを介した HTTP 基本アクセス認証スキームをサポートしています。このスキームを利用するには、base64(login:password)値を計算し、認証ヘッダーを次のように設定します。
connection.addHeader('Authorization', 'Basic amsudmNAbWFFR5ydTp5b3V0cmFjaw==');
ターゲットサーバーが認証に成功したときに Cookie を提供しない限り、すべての要求に対して認証ヘッダーを設定します。
HTTP Cookie は、存在する場合は内部で透過的に管理されます。つまり、REST 呼び出しが Cookie を返した場合、それらは自動的に保持され、期限が切れるまで同じドメインへのアクセスを提供します。ヘッダーにクッキーを手動で設定することもできます。
connection.addHeader('Cookie', 'MyServiceCookie=732423sdfs73242');
ケーススタディ
以下のケーススタディでは、ワークフロー REST API を使用して YouTrack を外部アプリケーションと統合する方法を説明します。
ペーストビンの統合
Pastebin(英語) は、テキストを一定期間オンラインで保存できる Web サイトです。コードスニペットやログファイルからの抽出など、任意のテキスト文字列を貼り付けることができます。
このケーススタディでは、新しい課題からコードスニペットを抽出し、代わりに Pastebin に保存します。課題の説明には、Pastebin に移動されたコンテンツへのリンクが保持されています。次のワークフロールールは、このシナリオの実装方法を示しています。
const entities = require('@jetbrains/youtrack-scripting-api/entities');
const http = require('@jetbrains/youtrack-scripting-api/http');
const workflow = require('@jetbrains/youtrack-scripting-api/workflow');
exports.rule = entities.Issue.onChange({
title: 'Export to Pastebin.com',
action: function (ctx) {
const issue = ctx.issue;
if (issue.becomesReported || (issue.isReported && issue.isChanged('description'))) {
// Find a code sample in issue description: the text between code markup tokens.
const findCode = function () {
const start = issue.description.indexOf('{code}');
if (start !== -1) {
const end = issue.description.indexOf('{code}', start + 1);
if (end !== -1) {
return issue.description.substring(start + 6, end);
}
}
return '';
};
const code = findCode();
if (code.length !== 0) {
const connection = new http.Connection('https://pastebin.com');
connection.addHeader('Content-Type', 'application/x-www-form-urlencoded');
// Pastebin accepts only forms, so we pack everything as form fields.
// Authentication of performed via api developer key.
const payload = [];
payload.push({name: 'api_option', value: 'paste'});
payload.push({name: 'api_dev_key', value: '98bcac75e1e327b54c08947ea1dbcb7e'});
payload.push({name: 'api_paste_private', value: 1});
payload.push({name: 'api_paste_name', value: 'Code sample from issue ' + issue.id});
payload.push({name: 'api_paste_code', value: code.trim()});
const response = connection.postSync('/api/api_post.php', [], payload);
if (response.code === 200 && response.response.indexOf('https://pastebin.com/') !== -1) {
const url = response.response;
issue.description = issue.description.replace('{code}' + code + '{code}',
'See sample at ' + url);
workflow.message('Code sample is moved at <a href="' + url + '">' + url + "</a>");
} else {
workflow.message('Failed to replace code due to: ' + response.response);
}
}
}
}
});
一方、逆のことをしたい場合もあります。出会った Pastebin
リンクをコードスニペットに展開する、つまり、ダウンロードして課題に挿入します。それをコーディングしてみましょう:
const entities = require('@jetbrains/youtrack-scripting-api/entities');
const http = require('@jetbrains/youtrack-scripting-api/http');
const workflow = require('@jetbrains/youtrack-scripting-api/workflow');
exports.rule = entities.Issue.onChange({
title: 'Import from Pastebin.com',
action: function (ctx) {
const issue = ctx.issue;
if (issue.becomesReported || (issue.isReported && issue.isChanged('description'))) {
const baseUrl = "https://pastebin.com/";
const urlBaseLength = baseUrl.length;
// Check, if issue description contains a link to pastebin.
const linkStart = issue.description.indexOf(baseUrl);
if (linkStart !== -1) {
// So we found a link, let's extract the key and download the contents via API.
const pasteKey = issue.description.substring(linkStart + urlBaseLength, linkStart + urlBaseLength + 8);
const connection = new http.Connection('https://pastebin.com');
const response = connection.getSync('/raw/' + pasteKey, []);
if (response.code === 200) {
const url = baseUrl + pasteKey;
issue.description = issue.description.replace(url, '{code}' + response.response + '{code}');
workflow.message('Code sample is moved from <a href="' + url + '">' + url + "</a>");
} else {
workflow.message('Failed to import code due to: ' + response.response);
}
}
}
}
});
Harvest Web サービスを使ったカスタムタイムトラッキング
YouTrack に記録した労働時間について顧客に請求したいとします。課題は、YouTrack が請求書を管理し、特定の顧客と過ごした時間を関連付けるために実際には構築されていないことです。専用のタイムトラッキングサービスとの統合は、人生をずっと楽にすることができます。
まず、以下のすべてのスクリプトに共通の部分を紹介しましょう。接続の初期化と共通のペイロードフィールドを含む共通のカスタムスクリプトです。
const http = require('@jetbrains/youtrack-scripting-api/http');
exports.userIds = {
'jane.smith': '1790518',
'john.black': '1703589'
};
exports.initConnection = function () {
const connection = new http.Connection('https://yourapp.harvestapp.com');
// see https://help.getharvest.com/api-v1/authentication/authentication/http-basic/
connection.addHeader('Authorization',
'Basic bXJzLm1hcml5YS8kYXZ5ZG94YUBnbWFpbC0jb206a3V6eWEyMDA0');
connection.addHeader('Accept', 'application/json');
connection.addHeader('Content-Type', 'application/json');
return connection;
};
exports.initPayload = function (user) {
return {
project_id: '14383202',
task_id: '8120350',
user_id: exports.userIds[user.login]
};
};
考えられるシナリオの 1 つは、カスタムフィールド(請求可能時間)を導入し、このフィールドの値に対する変更を Harvest Web サービスに投稿することです。
const entities = require('@jetbrains/youtrack-scripting-api/entities');
const workflow = require('@jetbrains/youtrack-scripting-api/workflow');
const common = require('./common');
exports.rule = entities.Issue.onChange({
title: 'Post Work Item',
action: function (ctx) {
const issue = ctx.issue;
if (issue.fields.isChanged(ctx.Hours)) {
const hours = (issue.fields.Hours || 0) - (issue.fields.oldValue(ctx.Hours) || 0);
const connection = common.initConnection();
const payload = common.initPayload(ctx.currentUser);
payload.hours = hours;
const response = connection.postSync('/daily/add', [], payload);
if (response && response.code === 201) {
workflow.message('A work item was added to Harvest!');
} else {
workflow.message('Something went wrong when adding a work item to Harvest: ' + response);
}
}
},
requirements: {
Hours: {
type: entities.Field.integerType,
name: 'Billable hours'
}
}
});
別のオプションを考えてみましょう。課題が進行中状態に移行したときにタイムトラッキングを開始し、課題が解決したときにタイムトラッキングを停止します。幸いなことに、Harvest にはタイマー API があり、これを使用してタイマーをリモートで開始および停止できます。タイマー識別子を格納するには、収穫 ID カスタムフィールドが必要です。
const entities = require('@jetbrains/youtrack-scripting-api/entities');
const workflow = require('@jetbrains/youtrack-scripting-api/workflow');
const common = require('./common');
exports.rule = entities.Issue.onChange({
title: 'Start Timer',
action: function (ctx) {
const issue = ctx.issue;
if (issue.fields.becomes(ctx.State, ctx.State['In Progress'])) {
const connection = common.initConnection();
const payload = common.initPayload(ctx.currentUser);
const response = connection.postSync('/daily/add', [], payload);
if (response && response.code === 201) {
issue.fields.HID = JSON.parse(response.response).id;
workflow.message('A timer is started at Harvest!');
} else {
workflow.message('Something went wrong when starting a timer at Harvest: ' + response);
}
}
},
requirements: {
HID: {
type: entities.Field.stringType,
name: 'Harvest ID'
},
State: {
type: entities.State.fieldType,
'In Progress': {}
}
}
});
次のワークフロールールは、課題が解決したときに Harvest タイマーを停止します。
const entities = require('@jetbrains/youtrack-scripting-api/entities');
const workflow = require('@jetbrains/youtrack-scripting-api/workflow');
const common = require('./common');
exports.rule = entities.Issue.onChange({
title: 'Stop Timer',
action: function (ctx) {
const issue = ctx.issue;
if (issue.becomesResolved && issue.fields.HID) {
const connection = common.initConnection();
const response = connection.getSync('/daily/timer/' + issue.fields.HID);
if (response && response.code === 200) {
workflow.message('A timer is stopped at Harvest!');
} else {
workflow.message('Something went wrong when stopping a timer at Harvest: ' + response);
}
}
},
requirements: {
HID: {
type: entities.Field.stringType,
name: 'Harvest ID'
}
}
});
multipart/form-data タイプのバイナリコンテンツの投稿
YouTrack からサードパーティアプリケーションにファイルを投稿する必要がある場合、ターゲットアプリケーションでは、Content-Type
ヘッダー値を multipart/form-data
に設定して POST 要求を行う必要がある場合があります。
YouTrack ワークフロールールからこのような要求を行うには、Connection.postSync
メソッドの payload
パラメーターのオブジェクトを渡し、その type
値を 'multipart/form-data'
に設定します。
YouTrack は、ペイロードの parts
パラメーターで、添付ファイル部分で構成される配列を見つけることを期待します。必要な数だけ部分を送信できます。
parts
配列の各要素には、次のフィールドを使用できます。
フィールド | タイプ | 説明 | 必須 |
---|
name | 文字列 | 部品の名前。 | ✓ |
size | 番号 | 添付ファイルのサイズ(バイト単位)。 | ✓ |
fileName | 文字列 | 添付ファイルの名前。 | ✓ |
content | InputStream | 文字列 | ファイルの内容。 contentType が明示的に設定されていない場合、YouTrack はコンテンツをバイナリ形式の InputStream として期待します。 | ✓ |
contentType | 文字列 | ファイルのコンテンツタイプ。 個々のパーツごとに、contentType 値を個別に設定できます。contentType 値に応じて、YouTrack は異なるタイプの content を想定します。例: contentType: 'application/json' を設定する場合、content 値は JSON 形式である必要があります。 | |
以下は、POST リクエストを作成し、multipart/form-data
タイプの添付ファイルを渡すワークフロールールの例です。
const entities = require('@jetbrains/youtrack-scripting-api/entities');
const http = require('@jetbrains/youtrack-scripting-api/http');
exports.rule = entities.Issue.action({
title: 'Reattach the first attachment',
// The base URL is taken from the first line of the issue description.
// Auth details are taken from the second of the issue description, the user ID - from the third line.
command: 'reattach',
guard: (ctx) => {
return true;
},
action: (ctx) => {
const issue = ctx.issue;
const baseURL = issue.description.split('\n')[0].trim()
const auth = issue.description.split('\n')[1].trim()
const rootUserId = issue.description.split('\n')[2].trim()
const connection = new http.Connection(baseURL);
const attachment = issue.attachments.first();
connection.addHeader('authorization', auth);
connection.postSync('issues/' + issue.id + '/attachments', [], {
type: 'multipart/form-data',
parts: [
{
name: 'my-part-name',
size: attachment.size,
fileName: 'filename',
content: attachment.content
}
]
});
},
requirements: {}
});