Using REST API Methods in JavaScript Workflows
Since release 7.0 YouTrack brings REST client implementation to the workflow API. You can use workflows to script push-style integrations with your favorite tools.
See complete API reference in Http module documentation.
Here's a basic example:
// Post issue content to third-party tool and add response as a comment
var connection = new http.Connection('http://server.com');
connection.addHeader('Content-Type', 'text/html');
var response = connection.postSync('/issueRegistry', [], issue.description);
if (response && response.code === 200) {
issue.addComment(response.response);
}
Authentication
The REST client supports the HTTP basic access authentication scheme via headers. To utilize this scheme, compute a base64(login:password) value and set the authorization header as follows:
connection.addHeader('Authorization', 'Basic amsudmNAbWFFR5ydTp5b3V0cmFjaw==');
Set the authorization header for every request, unless the target server provides cookies upon successful authentication.
HTTP cookies are managed transparently under the hood, when present. That is, if any REST call returns cookies, they persist automatically and provide access to the same domain until they expire. You can also set cookies manually in the header:
connection.addHeader('Cookie', 'MyServiceCookie=732423sdfs73242');
Server Response
The REST client returns the server response as an object, described in Response.
Secure Connections (SSL/TLS)
The REST client supports https://
connections out of the box. Although it's currently unable to present a client certificate during the handshake, it can still validate a server certificate against known certificate authorities. To learn more about adding trusted certificates to YouTrack, see SSL Certificates.
Best Practices
For best results, observe the following guidelines.
Know your protocol. If you're not yet familiar with HTTP, it's time to fill the gap. You should have at least a basic understanding of the protocol to script the integration and decrypt errors.
Know your API. Your favorite application that you're going to integrate with YouTrack almost certainly has documentation that tells you how to use their API. Check it out before you start to script an integration. For instance, here's a manual for the Pastebin service.
Use logging. Log errors and everything else with console.log(...)
.
Use a third-party REST client to make sure your requests are formatted correctly. Diagnostic tools in clients like cURL, Wget or the Postman extension for Chrome can help you to find out why your workflow is not acting as expected.
Don't forget to add Content-Type
and Accept
headers to your requests. The majority of APIs out there rely on these headers and refuse to work without them.
Case Studies
The following case studies illustrate how you can use the workflow REST API to integrate YouTrack with an external application.
Pastebin Integration
Pastebin is a website where you can store text online for a set period of time. You can paste any string of text like code snippets and extracts from log files.
In this case study, we extract code snippets from new issues and store them on Pastebin instead. The issue description retains a link to the content that is moved to Pastebin. The following workflow rule demonstrates how this scenario is implemented:
var entities = require('@jetbrains/youtrack-scripting-api/entities');
var http = require('@jetbrains/youtrack-scripting-api/http');
var workflow = require('@jetbrains/youtrack-scripting-api/workflow');
exports.rule = entities.Issue.onChange({
title: 'Export to Pastebin.com',
action: function(ctx) {
var issue = ctx.issue;
if (issue.becomesReported || (issue.isReported && issue.isChanged('description'))) {
// Find a code sample in issue description: the text between code markup tokens.
var findCode = function() {
var start = issue.description.indexOf('{code}');
if (start !== -1) {
var end = issue.description.indexOf('{code}', start + 1);
if (end !== -1) {
return issue.description.substring(start + 6, end);
}
}
return '';
};
var code = findCode();
if (code.length !== 0) {
var 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.
var 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()});
var response = connection.postSync('/api/api_post.php', [], payload);
if (response.code === 200 && response.response.indexOf('https://pastebin.com/') !== -1) {
var 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);
}
}
}
}
});
On the other hand, we may want to do the opposite: to expand any Pastebin link we met into a code snippet, i.e. to download it and insert into issue. Let's try to code it:
var entities = require('@jetbrains/youtrack-scripting-api/entities');
var http = require('@jetbrains/youtrack-scripting-api/http');
var workflow = require('@jetbrains/youtrack-scripting-api/workflow');
exports.rule = entities.Issue.onChange({
title: 'Import from Pastebin.com',
action: function(ctx) {
var issue = ctx.issue;
if (issue.becomesReported || (issue.isReported && issue.isChanged('description'))) {
var baseUrl = "https://pastebin.com/";
var urlBaseLength = baseUrl.length;
// Check, if issue description contains a link to pastebin.
var linkStart = issue.description.indexOf(baseUrl);
if (linkStart !== -1) {
// So we found a link, let's extract the key and download the contents via API.
var pasteKey = issue.description.substring(linkStart + urlBaseLength, linkStart + urlBaseLength + 8);
var connection = new http.Connection('https://pastebin.com');
var response = connection.getSync('/raw/' + pasteKey, []);
if (response.code === 200) {
var 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);
}
}
}
}
});
Custom Time Tracking with the Harvest Web Service
Suppose that we want to bill customers for the working hours that we record in YouTrack. The problem is that YouTrack isn't really built for managing invoices and associating spent time with specific customers. An integration with a dedicated time tracking service can make life a lot easier.
Let's first introduce a common part for all scripts below: a common custom script, containing connection initialization and common payload fields:
var http = require('@jetbrains/youtrack-scripting-api/http');
exports.userIds = {
'jane.smith': '1790518',
'john.black': '1703589'
};
exports.initConnection = function() {
var connection = new http.Connection('https://yourapp.harvestapp.com');
// see http://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]
};
};
One possible scenario is to introduce a custom field - Billable hours - and post changes to the value of this field to the Harvest web service.
var entities = require('@jetbrains/youtrack-scripting-api/entities');
var workflow = require('@jetbrains/youtrack-scripting-api/workflow');
var common = require('./common');
exports.rule = entities.Issue.onChange({
title: 'Post Work Item',
action: function(ctx) {
var issue = ctx.issue;
if (issue.fields.isChanged(ctx.Hours)) {
var hours = (issue.fields.Hours || 0) - (issue.fields.oldValue(ctx.Hours) || 0);
var connection = common.initConnection();
var payload = common.initPayload(ctx.currentUser);
payload.hours = hours;
var 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'
}
}
});
Let's consider another option: start time tracking when an issue moves to an In Progress state and stop time tracking when the issue is resolved. Lucky for us, Harvest has a timer API that we can use to start and stop the timers remotely. The Harvest ID custom field is required to store the timer identifier.
var entities = require('@jetbrains/youtrack-scripting-api/entities');
var workflow = require('@jetbrains/youtrack-scripting-api/workflow');
var common = require('./common');
exports.rule = entities.Issue.onChange({
title: 'Start Timer',
action: function(ctx) {
var issue = ctx.issue;
if (issue.fields.becomes(ctx.State, ctx.State['In Progress'])) {
var connection = common.initConnection();
var payload = common.initPayload(ctx.currentUser);
var 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': {}
}
}
});
The following workflow rule stops the Harvest timer when an issue is resolved.
var entities = require('@jetbrains/youtrack-scripting-api/entities');
var workflow = require('@jetbrains/youtrack-scripting-api/workflow');
var common = require('./common');
exports.rule = entities.Issue.onChange({
title: 'Stop Timer',
action: function(ctx) {
var issue = ctx.issue;
if (issue.becomesResolved && issue.fields.HID) {
var connection = common.initConnection();
var 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'
}
}
});
Last modified: 30 March 2021