Vmeda.Online/gulp/jira.js

477 lines
14 KiB
JavaScript

// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
const exec = require('child_process').exec;
const https = require('https');
const inquirer = require('inquirer');
const fs = require('fs');
const request = require('request'); // This lib is deprecated, but it was the only way I found to make upload files work.
const DevConfig = require('./dev-config');
const Git = require('./git');
const Url = require('./url');
const Utils = require('./utils');
const apiVersion = 2;
/**
* Class to interact with Jira.
*/
class Jira {
/**
* Ask the password to the user.
*
* @return Promise resolved with the password.
*/
async askPassword() {
const data = await inquirer.prompt([
{
type: 'password',
name: 'password',
message: `Please enter the password for the username ${this.username}.`,
},
]);
return data.password;
}
/**
* Ask the user the tracker data.
*
* @return Promise resolved with the data, rejected if cannot get.
*/
async askTrackerData() {
const data = await inquirer.prompt([
{
type: 'input',
name: 'url',
message: 'Please enter the tracker URL.',
default: 'https://tracker.moodle.org/',
},
{
type: 'input',
name: 'username',
message: 'Please enter your tracker username.',
},
]);
DevConfig.save({
'tracker.url': data.url,
'tracker.username': data.username,
});
return data;
}
/**
* Build URL to perform requests to Jira.
*
* @param uri URI to add the the Jira URL.
* @return URL.
*/
buildRequestUrl(uri) {
return Utils.concatenatePaths([this.url, this.uri, '/rest/api/', apiVersion, uri]);
}
/**
* Delete an attachment.
*
* @param attachmentId Attachment ID.
* @return Promise resolved when done.
*/
async deleteAttachment(attachmentId) {
const response = await this.request(`attachment/${attachmentId}`, 'DELETE');
if (response.status != 204) {
throw new Error('Could not delete the attachment');
}
}
/**
* Load the issue info from jira server using a REST API call.
*
* @param key Key to identify the issue. E.g. MOBILE-1234.
* @param fields Fields to get.
* @return Promise resolved with the issue data.
*/
async getIssue(key, fields) {
fields = fields || '*all,-comment';
await this.init(); // Initialize data if needed.
const response = await this.request(`issue/${key}`, 'GET', {'fields': fields, 'expand': 'names'});
if (response.status == 404) {
throw new Error('Issue could not be found.');
} else if (response.status != 200) {
throw new Error('The tracker is not available.')
}
const issue = response.data;
issue.named = {};
// Populate the named fields in a separate key. Allows us to easily find them without knowing the field ID.
const nameList = issue.names || {};
for (const fieldKey in issue.fields) {
if (nameList[fieldKey]) {
issue.named[nameList[fieldKey]] = issue.fields[fieldKey];
}
}
return issue
}
/**
* Load the version info from the jira server using a rest api call.
*
* @return Promise resolved when done.
*/
async getServerInfo() {
const response = await this.request('serverInfo');
if (response.status != 200) {
throw new Error(`Unexpected response code: ${response.status}`, response);
}
this.version = response.data;
}
/**
* Get tracker data to push an issue.
*
* @return Promise resolved with the data.
*/
async getTrackerData() {
// Check dev-config file first.
let data = this.getTrackerDataFromDevConfig();
if (data) {
console.log('Using tracker data from dev-config file');
return data;
}
// Try to use mdk now.
try {
data = await this.getTrackerDataFromMdk();
console.log('Using tracker data from mdk');
return data;
} catch (error) {
// MDK not available or not configured. Ask for the data.
const trackerData = await this.askTrackerData();
trackerData.fromInput = true;
return trackerData;
}
}
/**
* Get tracker data from dev config file.
*
* @return Data, undefined if cannot get.
*/
getTrackerDataFromDevConfig() {
const url = DevConfig.get('tracker.url');
const username = DevConfig.get('tracker.username');
if (url && username) {
return {
url,
username,
};
}
}
/**
* Get tracker URL and username from mdk.
*
* @return Promise resolved with the data, rejected if cannot get.
*/
getTrackerDataFromMdk() {
return new Promise((resolve, reject) => {
exec('mdk config show tracker.url', (err, url) => {
if (!url) {
reject(err || 'URL not found.');
return;
}
exec('mdk config show tracker.username', (error, username) => {
if (username) {
resolve({
url: url.replace('\n', ''),
username: username.replace('\n', ''),
});
} else {
reject(error || 'Username not found.');
}
});
});
});
}
/**
* Initialize some data.
*
* @return Promise resolved when done.
*/
async init() {
if (this.initialized) {
// Already initialized.
return;
}
// Get tracker URL and username.
let trackerData = await this.getTrackerData();
this.url = trackerData.url;
this.username = trackerData.username;
const parsed = Url.parse(this.url);
this.ssl = parsed.protocol == 'https';
this.host = parsed.domain;
this.uri = parsed.path;
// Get the password.
const keytar = require('keytar');
this.password = await keytar.getPassword('mdk-jira-password', this.username); // Use same service name as mdk.
if (!this.password) {
// Ask the user.
this.password = await this.askPassword();
}
while (!this.initialized) {
try {
await this.getServerInfo();
this.initialized = true;
keytar.setPassword('mdk-jira-password', this.username, this.password);
} catch (error) {
console.log('Error connecting to the server. Please make sure you entered the data correctly.', error);
if (trackerData.fromInput) {
// User entered the data manually, ask him again.
trackerData = await this.askTrackerData();
this.url = trackerData.url;
this.username = trackerData.username;
}
this.password = await this.askPassword();
}
}
}
/**
* Check if a certain issue could be a security issue.
*
* @param key Key to identify the issue. E.g. MOBILE-1234.
* @return Promise resolved with boolean: whether it's a security issue.
*/
async isSecurityIssue(key) {
const issue = await this.getIssue(key, 'security');
return issue.fields && !!issue.fields.security;
}
/**
* Sends a request to the server and returns the data.
*
* @param uri URI to add the the Jira URL.
* @param method Method to use. Defaults to 'GET'.
* @param params Params to send as GET params (in the URL).
* @param data JSON string with the data to send as POST/PUT params.
* @param headers Headers to send.
* @return Promise resolved with the result.
*/
request(uri, method, params, data, headers) {
uri = uri || '';
method = (method || 'GET').toUpperCase();
data = data || '';
params = params || {};
headers = headers || {};
headers['Content-Type'] = 'application/json';
return new Promise((resolve, reject) => {
// Build the request URL.
const url = Url.addParamsToUrl(this.buildRequestUrl(uri), params);
// Initialize the request.
const options = {
method: method,
auth: `${this.username}:${this.password}`,
headers: headers,
};
const buildRequest = https.request(url, options);
// Add data.
if (data) {
buildRequest.write(data);
}
// Treat response.
buildRequest.on('response', (response) => {
// Read the result.
let result = '';
response.on('data', (chunk) => {
result += chunk;
});
response.on('end', () => {
try {
result = JSON.parse(result);
} catch (error) {
// Leave it as text.
}
resolve({
status: response.statusCode,
data: result,
});
});
});
buildRequest.on('error', (e) => {
reject(e);
});
// Send the request.
buildRequest.end();
});
}
/**
* Sets a set of fields for a certain issue in Jira.
*
* @param issueId Key to identify the issue. E.g. MOBILE-1234.
* @param updates Object with the fields to update.
* @return Promise resolved when done.
*/
async setCustomFields(issueId, updates) {
const issue = await this.getIssue(issueId);
const update = {'fields': {}};
// Detect which fields have changed.
for (const updateName in updates) {
const updateValue = updates[updateName];
const remoteValue = issue.named[updateName];
if (!remoteValue || remoteValue != updateValue) {
// Map the label of the field with the field code.
let fieldKey;
for (const id in issue.names) {
if (issue.names[id] == updateName) {
fieldKey = id;
break;
}
}
if (!fieldKey) {
throw new Error(`Could not find the field named ${updateName}.`);
}
update.fields[fieldKey] = updateValue;
}
}
if (!Object.keys(update.fields).length) {
// No fields to update.
console.log('No updates required.')
return;
}
const response = await this.request(`issue/${key}`, 'PUT', null, JSON.stringify(update));
if (response.status != 204) {
throw new Error(`Issue was not updated: ${response.status}`, response.data);
}
console.log('Issue updated successfully.');
}
/**
* Upload a new attachment to an issue.
*
* @param key Key to identify the issue. E.g. MOBILE-1234.
* @param filePath Path to the file to upload.
* @return Promise resolved when done.
*/
async upload(key, filePath) {
const uri = `issue/${key}/attachments`;
const headers = {
'X-Atlassian-Token': 'nocheck',
}
const response = await this.uploadFile(uri, 'file', filePath, headers);
if (response.status != 200) {
throw new Error('Could not upload file to Jira issue');
}
console.log('File successfully uploaded.')
}
/**
* Upload a file to Jira.
*
* @param uri URI to add the the Jira URL.
* @param fieldName Name of the form field where to put the file.
* @param filePath Path to the file.
* @param headers Headers.
* @return Promise resolved with the result.
*/
async uploadFile(uri, fieldName, filePath, headers) {
uri = uri || '';
headers = headers || {};
headers['Content-Type'] = 'multipart/form-data';
return new Promise((resolve) => {
// Add the file to the form data.
const formData = {};
formData[fieldName] = {
value: fs.createReadStream(filePath),
options: {
filename: filePath.substr(filePath.lastIndexOf('/') + 1),
contentType: 'multipart/form-data',
},
};
// Perform the request.
const options = {
url: this.buildRequestUrl(uri),
method: 'POST',
headers: headers,
auth: {
user: this.username,
pass: this.password,
},
formData: formData,
};
request(options, (_err, httpResponse, body) => {
resolve({
status: httpResponse.statusCode,
data: body,
});
});
});
}
}
module.exports = new Jira();