YouTrack および Hub ヘルプの開発者ポータル

JavaScript ワークフローでの REST API メソッドの使用

YouTrack は、ワークフロー API への REST クライアントの実装をサポートします。ワークフローを使用して、お気に入りのツールとのプッシュスタイルの統合をスクリプト化できます。

HTTP モジュールドキュメントの完全な API リファレンスを参照してください。

これが基本的な例です。

// 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 クライアントは、レスポンスに従って、サーバー応答をオブジェクトとして返します。

安全な接続 (SSL/TLS)

REST クライアントは、すぐに https:// 接続をサポートします。現在ハンドシェイク中にクライアント証明書を提示することはできませんが、それでも既知の認証局に対してサーバー証明書を検証できます。信頼できる証明書を YouTrack に追加する方法の詳細については、SSL 証明書を参照してください。

ベストプラクティス

最良の結果を得るためには、次のガイドラインに従ってください。

  1. プロトコルを知ってください。まだ HTTP に慣れていないのであれば、ギャップを埋める時間です。統合をスクリプト化してエラーを復号化するには、少なくともプロトコルについての基本的な知識が必要です。

  2. API を知ってください。YouTrack と統合しようとしているお気に入りのアプリケーションには、ほぼ確実にそれらの API の使用方法を説明したドキュメントがあります。統合のスクリプトを作成する前に、チェックしてください。たとえば、ペーストビンサービス(英語)のマニュアルです。

  3. ロギングを使用してください。console.log(...) でエラーとそれ以外のすべてのものを記録します。

  4. リクエストが正しくフォーマットされていることを確認するには、サードパーティの REST クライアントを使用してください。cURLWget、Chrome 用の Postman 拡張などのクライアントの診断ツールを使用すると、ワークフローが期待どおりに機能しない理由を見つけることができます。

  5. Content-TypeAccept ヘッダーをあなたのリクエストに追加することを忘れないでください。そこにある API の大部分はこれらのヘッダーに頼っていて、それらなしで動作することを拒否します。

ケーススタディ

以下のケーススタディでは、ワークフロー 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: {} });