MOBILE-3501 gulp: Support uploading patches to Jira
parent
719ffa2ff1
commit
05fa577577
33
gulp/git.js
33
gulp/git.js
|
@ -13,6 +13,7 @@
|
|||
// limitations under the License.
|
||||
|
||||
const exec = require('child_process').exec;
|
||||
const fs = require('fs');
|
||||
const DevConfig = require('./dev-config');
|
||||
const Utils = require('./utils');
|
||||
|
||||
|
@ -21,6 +22,38 @@ const Utils = require('./utils');
|
|||
*/
|
||||
class Git {
|
||||
|
||||
/**
|
||||
* Create a patch.
|
||||
*
|
||||
* @param range Show only commits in the specified revision range.
|
||||
* @param saveTo Path to the file to save the patch to. If not defined, the patch contents will be returned.
|
||||
* @return Promise resolved when done. If saveTo not provided, it will return the patch contents.
|
||||
*/
|
||||
createPatch(range, saveTo) {
|
||||
return new Promise((resolve, reject) => {
|
||||
exec(`git format-patch ${range} --stdout`, (err, result) => {
|
||||
if (err) {
|
||||
reject(err || 'Cannot create patch.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!saveTo) {
|
||||
resolve(result);
|
||||
return;
|
||||
}
|
||||
|
||||
// Save it to a file.
|
||||
const directory = saveTo.substring(0, saveTo.lastIndexOf('/'));
|
||||
if (directory && directory != '.' && directory != '..' && !fs.existsSync(directory)) {
|
||||
fs.mkdirSync(directory);
|
||||
}
|
||||
fs.writeFileSync(saveTo, result);
|
||||
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current branch.
|
||||
*
|
||||
|
|
128
gulp/jira.js
128
gulp/jira.js
|
@ -16,6 +16,8 @@ const exec = require('child_process').exec;
|
|||
const https = require('https');
|
||||
const keytar = require('keytar');
|
||||
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');
|
||||
|
@ -73,6 +75,30 @@ class Jira {
|
|||
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.
|
||||
*
|
||||
|
@ -248,6 +274,18 @@ class Jira {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
|
@ -269,16 +307,23 @@ class Jira {
|
|||
return new Promise((resolve, reject) => {
|
||||
|
||||
// Build the request URL.
|
||||
let url = Utils.concatenatePaths([this.url, this.uri, '/rest/api/', apiVersion, uri]);
|
||||
url = Url.addParamsToUrl(url, params);
|
||||
const url = Url.addParamsToUrl(this.buildRequestUrl(uri), params);
|
||||
|
||||
// Perform the request.
|
||||
// Initialize the request.
|
||||
const options = {
|
||||
method: method,
|
||||
auth: `${this.username}:${this.password}`,
|
||||
headers: headers,
|
||||
};
|
||||
const request = https.request(url, options, (response) => {
|
||||
const request = https.request(url, options);
|
||||
|
||||
// Add data.
|
||||
if (data) {
|
||||
request.write(data);
|
||||
}
|
||||
|
||||
// Treat response.
|
||||
request.on('response', (response) => {
|
||||
// Read the result.
|
||||
let result = '';
|
||||
response.on('data', (chunk) => {
|
||||
|
@ -302,11 +347,7 @@ class Jira {
|
|||
reject(e);
|
||||
});
|
||||
|
||||
// Send data.
|
||||
if (data) {
|
||||
request.write(data);
|
||||
}
|
||||
|
||||
// Send the request.
|
||||
request.end();
|
||||
});
|
||||
}
|
||||
|
@ -359,6 +400,75 @@ class Jira {
|
|||
|
||||
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, reject) => {
|
||||
// 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();
|
||||
|
|
|
@ -24,6 +24,56 @@ const Utils = require('./utils');
|
|||
*/
|
||||
class PushTask {
|
||||
|
||||
/**
|
||||
* Push a patch to the tracker and remove the previous one.
|
||||
*
|
||||
* @param branch Branch name.
|
||||
* @param branchData Parsed branch data.
|
||||
* @param remote Remote used.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
async pushPatch(branch, branchData, remote) {
|
||||
const headCommit = await Git.getHeadCommit(branch, branchData);
|
||||
|
||||
if (!headCommit) {
|
||||
throw new Error('Head commit not resolved, abort pushing patch.');
|
||||
}
|
||||
|
||||
// Create the patch file.
|
||||
const fileName = branch + '.patch';
|
||||
const tmpPatchPath = `./tmp/${fileName}`;
|
||||
|
||||
await Git.createPatch(`${headCommit}...${branch}`, tmpPatchPath);
|
||||
console.log('Git patch created');
|
||||
|
||||
// Check if there is an attachment with same name in the issue.
|
||||
const issue = await Jira.getIssue(branchData.issue, 'attachment');
|
||||
|
||||
let existingAttachmentId;
|
||||
const attachments = (issue.fields && issue.fields.attachment) || [];
|
||||
for (const i in attachments) {
|
||||
if (attachments[i].filename == fileName) {
|
||||
// Found an existing attachment with the same name, we keep track of it.
|
||||
existingAttachmentId = attachments[i].id;
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Push the patch to the tracker.
|
||||
console.log(`Uploading patch ${fileName} to the tracker...`);
|
||||
await Jira.upload(branchData.issue, tmpPatchPath);
|
||||
|
||||
if (existingAttachmentId) {
|
||||
// On success, deleting file that was there before.
|
||||
try {
|
||||
console.log('Deleting older patch...')
|
||||
await Jira.deleteAttachment(existingAttachmentId);
|
||||
} catch (error) {
|
||||
console.log('Could not delete older attachment.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the task.
|
||||
*
|
||||
|
@ -46,7 +96,9 @@ class PushTask {
|
|||
throw new Error('Cannot push HEAD branch');
|
||||
}
|
||||
|
||||
const keepRunning = await this.validateLastCommitMessage(branch);
|
||||
// Parse the branch to get the project and issue number.
|
||||
const branchData = Utils.parseBranch(branch);
|
||||
const keepRunning = await this.validateLastCommitMessage(branchData);
|
||||
|
||||
if (!keepRunning) {
|
||||
// Last commit not valid, stop.
|
||||
|
@ -55,13 +107,31 @@ class PushTask {
|
|||
return;
|
||||
}
|
||||
|
||||
if (!args.patch) {
|
||||
// Check if it's a security issue to force patch mode.
|
||||
try {
|
||||
args.patch = await Jira.isSecurityIssue(branchData.issue);
|
||||
|
||||
if (args.patch) {
|
||||
console.log(`${branchData.issue} appears to be a security issue, switching to patch mode...`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`Could not check if ${branchData.issue} is a security issue.`);
|
||||
}
|
||||
}
|
||||
|
||||
if (args.patch) {
|
||||
// Create and upload a patch file.
|
||||
await this.pushPatch(branch, branchData, remote);
|
||||
} else {
|
||||
// Push the branch.
|
||||
console.log(`Pushing branch ${branch} to remote ${remote}...`);
|
||||
await Git.push(remote, branch, force);
|
||||
|
||||
// Update tracker info.
|
||||
console.log(`Branch pushed, update tracker info...`);
|
||||
await this.updateTrackerGitInfo(branch, remote);
|
||||
await this.updateTrackerGitInfo(branch, branchData, remote);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
@ -73,13 +143,11 @@ class PushTask {
|
|||
* Update git info in the tracker issue.
|
||||
*
|
||||
* @param branch Branch name.
|
||||
* @param branchData Parsed branch data.
|
||||
* @param remote Remote used.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
async updateTrackerGitInfo(branch, remote) {
|
||||
// Parse the branch to get the project and issue number.
|
||||
const branchData = Utils.parseBranch(branch);
|
||||
|
||||
async updateTrackerGitInfo(branch, branchData, remote) {
|
||||
// Get the repository data for the project.
|
||||
let repositoryUrl = DevConfig.get(branchData.project + '.repositoryUrl');
|
||||
let diffUrlTemplate = DevConfig.get(branchData.project + '.diffUrlTemplate', '');
|
||||
|
@ -134,12 +202,10 @@ class PushTask {
|
|||
/**
|
||||
* Validate last commit message comparing it with the branch name.
|
||||
*
|
||||
* @param branch Branch name.
|
||||
* @param branchData Parsed branch data.
|
||||
* @return True if value is ok or the user wants to continue anyway, false to stop.
|
||||
*/
|
||||
async validateLastCommitMessage(branch) {
|
||||
const branchData = Utils.parseBranch(branch);
|
||||
|
||||
async validateLastCommitMessage(branchData) {
|
||||
const messages = await Git.messages(1);
|
||||
const message = messages[0];
|
||||
|
||||
|
|
|
@ -2259,9 +2259,9 @@
|
|||
"integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg="
|
||||
},
|
||||
"aws4": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz",
|
||||
"integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ=="
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.10.0.tgz",
|
||||
"integrity": "sha512-3YDiu347mtVtjpyV3u5kVqQLP242c06zwDOgpeRnybmXlYYsLbtTrUBUm8i8srONt+FWobl5aibnU1030PeeuA=="
|
||||
},
|
||||
"babel-code-frame": {
|
||||
"version": "6.26.0",
|
||||
|
@ -5356,8 +5356,7 @@
|
|||
"extend": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
||||
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
|
||||
"dev": true
|
||||
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
|
||||
},
|
||||
"extend-shallow": {
|
||||
"version": "3.0.2",
|
||||
|
@ -6179,16 +6178,6 @@
|
|||
"resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
|
||||
"integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE="
|
||||
},
|
||||
"form-data": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
|
||||
"integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==",
|
||||
"requires": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.6",
|
||||
"mime-types": "^2.1.12"
|
||||
}
|
||||
},
|
||||
"formidable": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.2.tgz",
|
||||
|
@ -8400,29 +8389,6 @@
|
|||
"requires": {
|
||||
"ajv": "^6.5.5",
|
||||
"har-schema": "^2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"ajv": {
|
||||
"version": "6.9.1",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.9.1.tgz",
|
||||
"integrity": "sha512-XDN92U311aINL77ieWHmqCcNlwjoP5cHXDxIxbf2MaPYuCXOHS7gHH8jktxeK5omgd52XbSTX6a4Piwd1pQmzA==",
|
||||
"requires": {
|
||||
"fast-deep-equal": "^2.0.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
"json-schema-traverse": "^0.4.1",
|
||||
"uri-js": "^4.2.2"
|
||||
}
|
||||
},
|
||||
"fast-deep-equal": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
|
||||
"integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk="
|
||||
},
|
||||
"json-schema-traverse": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
||||
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"has": {
|
||||
|
@ -12117,7 +12083,8 @@
|
|||
"punycode": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
|
||||
"integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4="
|
||||
"integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=",
|
||||
"dev": true
|
||||
},
|
||||
"q": {
|
||||
"version": "1.5.1",
|
||||
|
@ -12500,9 +12467,9 @@
|
|||
}
|
||||
},
|
||||
"request": {
|
||||
"version": "2.88.0",
|
||||
"resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz",
|
||||
"integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==",
|
||||
"version": "2.88.2",
|
||||
"resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz",
|
||||
"integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==",
|
||||
"requires": {
|
||||
"aws-sign2": "~0.7.0",
|
||||
"aws4": "^1.8.0",
|
||||
|
@ -12511,7 +12478,7 @@
|
|||
"extend": "~3.0.2",
|
||||
"forever-agent": "~0.6.1",
|
||||
"form-data": "~2.3.2",
|
||||
"har-validator": "~5.1.0",
|
||||
"har-validator": "~5.1.3",
|
||||
"http-signature": "~1.2.0",
|
||||
"is-typedarray": "~1.0.0",
|
||||
"isstream": "~0.1.2",
|
||||
|
@ -12521,15 +12488,20 @@
|
|||
"performance-now": "^2.1.0",
|
||||
"qs": "~6.5.2",
|
||||
"safe-buffer": "^5.1.2",
|
||||
"tough-cookie": "~2.4.3",
|
||||
"tough-cookie": "~2.5.0",
|
||||
"tunnel-agent": "^0.6.0",
|
||||
"uuid": "^3.3.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"extend": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
||||
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
|
||||
"form-data": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
|
||||
"integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==",
|
||||
"requires": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.6",
|
||||
"mime-types": "^2.1.12"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -14407,12 +14379,19 @@
|
|||
}
|
||||
},
|
||||
"tough-cookie": {
|
||||
"version": "2.4.3",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz",
|
||||
"integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==",
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
|
||||
"integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==",
|
||||
"requires": {
|
||||
"psl": "^1.1.24",
|
||||
"punycode": "^1.4.1"
|
||||
"psl": "^1.1.28",
|
||||
"punycode": "^2.1.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"punycode": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
|
||||
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"tree-kill": {
|
||||
|
|
|
@ -159,6 +159,7 @@
|
|||
"minimist": "^1.2.5",
|
||||
"native-run": "^1.0.0",
|
||||
"node-loader": "^0.6.0",
|
||||
"request": "^2.88.2",
|
||||
"through": "^2.3.8",
|
||||
"typescript": "~2.6.2",
|
||||
"vinyl": "^2.2.0",
|
||||
|
|
Loading…
Reference in New Issue