Skip to content

Instantly share code, notes, and snippets.

@jonasfj
Created January 20, 2017 19:24
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jonasfj/c2e7cd4e0602073cb5d0e227fc4c3ec2 to your computer and use it in GitHub Desktop.
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.
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)
@jonasfj
Copy link
Author

jonasfj commented Jan 20, 2017

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment