Created
January 20, 2017 19:24
-
-
Save jonasfj/c2e7cd4e0602073cb5d0e227fc4c3ec2 to your computer and use it in GitHub Desktop.
Untested example code demonstrating how to use the `public/actions.json` artifact to display in-tree action in treeherder.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import _ from 'lodash'; | |
import Ajv from 'ajv'; | |
import assert from 'assert'; | |
import taskcluster from 'taskcluster-client'; | |
let data = {}; // TODO: Load actions.json from decision task | |
if (data.version > 1) { | |
throw Error('Unsupported version of public/actions.json'); | |
} | |
// Ignore unknown kinds, and ensure context is present with default value | |
let actions = data.actions | |
.filter(a => a.kind === 'task') | |
.map(a => _.defaults(a, {context: []})); | |
// Actions for the task-group are those with {context: [], ...} | |
let taskGroupActions = actions.filter(a => a.context.length === 0); | |
// Get actions for a given task | |
let taskActions = (task) => { | |
assert(task.tags instanceof Object, '`task` must be a task definition'); | |
return actions.filter(action => _.some(action.context, tagset => { | |
// if there is some tagset in action.context, such that every key-value pair | |
// from the tagset exists in task.tags, then the action is relevant | |
return _.every(tagset, (val, key) => task.tags[key] === val); | |
})); | |
} | |
// Render string given context | |
let renderString = (value, context) => { | |
return value.replace(/\${([^}]+)}/g, (expr, key) => { | |
if (context[key] === undefined) { | |
throw new Error('Undefined variable referenced in: ' + expr); | |
} | |
if (!_.includes(['number', 'string'], typeof(context[key]))) { | |
throw new Error('Cannot interpolate variable in: ' + expr); | |
} | |
return context[key]; | |
}); | |
}; | |
// Regular expression matching a timespan on the form: | |
// X days Y hours Z minutes | |
const timespanExpression = new RegExp([ | |
'^(\\s*(-|\\+))?', | |
'(\\s*(\\d+)\\s*d(ays?)?)?', | |
'(\\s*(\\d+)\\s*h((ours?)|r)?)?', | |
'(\\s*(\\d+)\\s*min(utes?)?)?', | |
'\\s*$', | |
].join(''), 'i'); | |
// Render timespan fromNow as JSON timestamp | |
let fromNow = (timespan = '', reference = Date.now()) => { | |
let m = timespanExpression.exec(timespan); | |
if (!m) { | |
throw new Error('Invalid timespan expression: ' + timespan); | |
} | |
let neg = (m[2] === '-' ? - 1 : 1); | |
let days = parseInt(m[4] || 0); | |
let hours = parseInt(m[7] || 0); | |
let minutes = parseInt(m[11] || 0); | |
return new Date( | |
reference + neg * ((days * 24 + hours) * 60 + minutes) * 60 * 1000, | |
).toJSON(); | |
}; | |
// Render JSON template | |
let render = (template, context) => _.cloneDeepWith(template, value => { | |
if (typeof(value) === 'string') { | |
return renderString(value, context); | |
} | |
if (typeof(value) !== 'object' || value instanceof Array) { | |
return undefined; // Return undefined to apply recursively | |
} | |
// Replace {$eval: 'variable'} with variable | |
if (value['$eval']) { | |
if (typeof(value['$eval'] !== 'string') { | |
throw new Error('$eval cannot carry non-string expression'); | |
} | |
if (context[value['$eval']] !== undefined) { | |
throw new Error('Undefined variable in $eval: ' value['$eval']); | |
} | |
return context[value['$eval']]; | |
} | |
// Replace {$dump: value} with JSON.stringify(value) | |
if (value['$dump']) { | |
return JSON.stringify(render(value['$dump'], context)); | |
} | |
// Replace {$fromNow: 'timespan'} with a JSON timestamp | |
if (value['$fromNow']) { | |
let timespan = render(value['$fromNow'], context); | |
if (typeof(timespan) !== 'string') { | |
throw new Error('$fromNow must be given a timespan as string'); | |
} | |
return fromNow(timespan); | |
} | |
// Apply string interpolation to keys, and recursively render all values | |
return _.reduce(value, (result, value, key) => { | |
result[renderString(key, context)] = render(value, context); | |
return result; | |
}, {}); | |
}); | |
// Trigger an action given taskGroupId, and | |
let triggerAction = async (action, taskGroupId, taskId = null, task = null) => { | |
assert(action.context.length === 0 || (taskId && task), 'expected a task'); | |
// Collect input from user, if required | |
let input = null; | |
if (action.schema) { | |
// TODO: Don't use prompt in production, hehe | |
input = JSON.parse(prompt('Enter JSON input for the action:')); | |
let ajv = new Ajv(); | |
assert(ajv.validate(action.schema, input), 'Invalid input'); | |
} | |
// Parameterize task | |
let actionTask = render(action.task, {taskGroupId, taskId, task, input}); | |
// Create queue client | |
let queue = new taskcluster.Queue({ | |
// TODO: Obtain temporary taskcluster credentials from taskcluster-login | |
}); | |
// Create action task | |
let actionTaskId = slugid.Nice(); | |
await queue.createTask(actionTaskId, actionTask); | |
// Return taskId that should be monitored, or somehow referenced | |
return actionTaskId; | |
}; | |
// Use this as follows: | |
// - Display actions from taskGroupActions in context-menu for task-group | |
// - Display actions from taskActions(task) in context-menu for task | |
// - Trigger an action using triggerAction(action, taskGroupId, taskId, task) | |
// - Monitor or link to taskId returned by triggerAction | |
// Things to do: | |
// - Load public/actions.json from the decision task | |
// - Obtain taskcluster credentials from taskcluster-login | |
// - Don't use window.prompt to obtain JSON input | |
// - Handle errors and display them nicely | |
// (probably this should be refactored and global variables should be avoided) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@hammad13060,
The
render
function above is what I plan to use for in-tree actions initially... Once json-e is done I hope we can replace it with json-e.This way making json-e super powerful won't block all the possible use-cases we have. Those use-cases will just a small subset of json-e until json-e is done.
Do you agree that the
render(template, context)
function is a strict subset of json-e? Or have I made any mistakes.