ado-release-notes
Version:
Creates release notes from two build numbers and uploads to a wiki
289 lines (244 loc) • 7.83 kB
JavaScript
var request = require('request-promise');
const mustache = require('mustache');
const yargs = require('yargs');
const dot = require('dot-object');
dot.override = true;
const args = yargs
.option('proxy', {
alias: 'p',
description: 'e.g. http://proxy.root.local:8080',
default: ''
})
.option('pat', {
description: 'personal access token from ADO',
})
.option('organisation', {
description: 'ADO organisation'
})
.option('project', {
description: 'ADO project'
})
.option('from', {
alias: 'f',
description: 'From BuildId'
})
.option('to', {
alias: 't',
description: 'To BuildId'
})
.option('buildname', {
alias: 'n',
description: "Build Name"
})
.option('template', {
description: "template file (containing mustache template)"
})
.option('wikiIdentifier', {
description: "ADO wiki Identifier"
})
.option('wikiPagePath', {
description: "ADO page path (to create a wiki page for the release notes"
})
.option('wikipat', {
description: 'personal access token from ADO',
})
.option('wikiorganisation', {
description: 'ADO organisation'
})
.option('wikiproject', {
description: 'ADO project'
})
.coerce('template', function (arg) {
return require('fs').readFileSync(arg, 'utf8')
})
.help()
.alias('help', 'h')
.demandOption(['pat', 'organisation', 'project', 'from', 'to', 'buildname'])
.argv;
// Default wiki orgs to main one if not provided
if (!args.wikiorganisation) {
args.wikiorganisation = args.organisation;
}
if (!args.wikiproject) {
args.wikiproject = args.project;
}
if (!args.wikipat) {
args.wikipat = args.pat;
}
const differentWiki = args.wikiorganisation !== args.organisation;
// Replace <img src= etc
async function resolveHtml(html) {
if (differentWiki) {
// Replace any img src
let start = 0;
while (start >= 0) {
start = html.indexOf('<img ', start);
if (start >= 0) {
let end = html.indexOf('>', start);
// Look for the src
let startsrc = html.indexOf('src=', start);
if (startsrc >= 0 && startsrc < end) {
let startquote = startsrc + 5;
let endquote = html.indexOf(html[startsrc + 4], startsrc + 5);
src = html.substring(startquote, endquote);
// If it points at azure - add our pat and go get it
if (src.startsWith("https://dev.azure.com")) {
src = src.replace('https://dev.azure.com', 'https://:' + args.pat + '@dev.azure.com');
var img;
// Get the image
await request({
'url': src,
'method': "GET",
'encoding': null,
'resolveWithFullResponse': true,
'proxy': args.proxy,
}).then(res => {
img = res;
}).catch(err => { throw err });
// Upload image it to the other wiki
await request({
'url': 'https://:' + args.wikipat + '@dev.azure.com/' + args.wikiorganisation + '/' + args.wikiproject + '/_apis/wit/attachments?api-version=5.1',
'method': "POST",
'proxy': args.proxy,
'content-type': img.headers['content-type'],
body: img.body
}).then(res => {
res = JSON.parse(res);
// Replace the src with new data uri
html = html.substring(0, startquote) + res.url + html.substring(endquote);
// Adjust the end of the tag accordingly
end = end + (res.url.length - src.length);
}).catch(err => { throw err });
}
}
start = end;
}
}
}
return html;
}
// Recursively replace
async function resolveHtmlValues(obj) {
// recurse down
var i;
if (obj instanceof Object) {
for (i in obj) {
if (obj.hasOwnProperty(i)) {
//recursive call to scan property
if (obj[i] instanceof Object) {
await resolveHtmlValues(obj[i]);
}
else {
//not an Object so obj[k] here is a value
if (typeof obj[i] === "string" && obj[i].startsWith("<")) {
obj[i] = await resolveHtml(obj[i]);
}
}
}
}
}
}
// Do the template merge
function merge(items) {
// If we have a template use it otherwise just spit out json
if (args.template) {
var html = mustache.to_html(args.template, items);
return html;
}
else {
return (JSON.stringify(items, null, 2));
}
}
// Resolve a value's item
async function resolveWorkItem(value) {
await request({
'url': 'https://:' + args.pat + '@dev.azure.com/' + args.organisation + '/' + args.project + '/_apis/wit/workitems/' + value.id + '?$expand=all',
'method': "GET",
'proxy': args.proxy,
'json': true
}).then(item => {
value.item = item;
}).catch(err => { throw err });
// dot.object expands out fields with dots in it so mustache can handle it
// E.g. "System.Title" become System: {Title: }
if (value.item.fields) {
value.item.fields = dot.object(value.item.fields);
}
// Look for any html in fields and resolve img tags
await resolveHtmlValues(value.item);
}
// Report an error
function reportError(error, response) {
var message = "";
if (response) {
message += "StatusCode: " + response.statusCode + "\r\n"
}
if (error) {
message += "Error: " + error.message + "\r\n";
}
return message;
}
let md = ""
// Get all work items
request({
'url': 'https://:' + args.pat + '@dev.azure.com/' + args.organisation + '/' + args.project + '/_apis/build/workitems?fromBuildId=' + args.from + '&toBuildId=' + args.to + '&api-version=5.1-preview.2',
'method': "GET",
'proxy': args.proxy,
'json': true
}).then(async items => {
// Add additional item (build refs, build name)
items.from = args.from;
items.to = args.to;
items.buildname = args.buildname;
for (let i = 0; i < items.value.length; i++) {
await resolveWorkItem(items.value[i]);
}
// Do the merge
md = merge(items);
// Upload it to a wiki?
if (args.wikiIdentifier) {
// Does this page already exist?
let pageId = 0;
let method = 'PUT';
let headers = {
"Content-Type": "application/json"
};
await request({
'url': 'https://:' + args.wikipat + '@dev.azure.com/' + args.wikiorganisation + '/' + args.wikiproject + '/_apis/wiki/wikis/' + args.wikiIdentifier + '/pages?path=' + args.wikiPagePath + '&api-version=5.1-preview.1',
'method': "GET",
'proxy': args.proxy,
'headers': {
"Content-Type": "application/json"
},
resolveWithFullResponse: true,
json: true
}).then(resp => {
pageId = resp.body.id;
method = "PATCH";
headers["If-Match"] = resp.headers.etag;
}).catch(err => {
// It is ok if the page doesn't exist yet
if (err.statusCode !== 404) {
throw err;
}
})
let createUrl = 'https://:' + args.wikipat + '@dev.azure.com/' + args.wikiorganisation + '/' + args.wikiproject + '/_apis/wiki/wikis/' + args.wikiIdentifier + '/pages';
if (pageId) {
createUrl += '/' + pageId + '?api-version=5.1-preview.1';
} else {
createUrl += '?path=' + args.wikiPagePath + '&api-version=5.1-preview.1';
}
request({
'url': createUrl,
'method': method,
'proxy': args.proxy,
'headers': headers,
json: { "content": md }
})
}
else {
console.log(md)
}
}).catch(err => {
throw err;
})