MOBILE-3501 gulp: Support uploading patches to Jira

main
Dani Palou 2020-07-27 10:37:20 +02:00
parent 719ffa2ff1
commit 05fa577577
5 changed files with 266 additions and 77 deletions

View File

@ -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.
*

View File

@ -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();

View File

@ -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;
}
// Push the branch.
console.log(`Pushing branch ${branch} to remote ${remote}...`);
await Git.push(remote, branch, force);
if (!args.patch) {
// Check if it's a security issue to force patch mode.
try {
args.patch = await Jira.isSecurityIssue(branchData.issue);
// Update tracker info.
console.log(`Branch pushed, update tracker info...`);
await this.updateTrackerGitInfo(branch, remote);
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, 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];

85
package-lock.json generated
View File

@ -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": {

View File

@ -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",