Task/jenkins 37519 fetch capabilities (#488)

* [JENKINS-37519] CapabilitiesStore that can load and cache capabilities from server; ported over from other branch

* [JENKINS-37519] let the CapabilityStore delegate to an CapabilityApi for remote calls; tests for CapabilityStore

* [JENKINS-37519] start of CapabilityAugmenter: basic tree walking in place; needs more filtering, proper fetch integration, actual tests

* [JENKINS-37519] better names for things in augmenter; tests

* [JENKINS-37519] finish impl of augmentCapabilities; write a basic test for it

* [JENKINS-37519] delint

* [JENKINS-37519] add another more elaborate test case for 'augmentCapabilities'

* [JENKINS-37519] handle the scenario where the capabilities weren't loaded more gracefully (and warn)

* [JENKINS-37519] add an "enum" of a few available capabilities

* [JENKINS-37519] export an instance of CapabilityAugmenter for use by clients

* [JENKINS-37519] fix a bug in the augmenter where a cycle caused an infinite loop

* [JENKINS-37519] add some perf logging so we can benchmark

* [JENKINS-37519] jsdoc

* [JENKINS-37519] add the ability to conditionally include the 'actions' property (since the data can be quite large)

* [JENKINS-37519] add "Capable" utility so we can attach a "can" method to each object; make available in Augmenter as well

* [JENKINS-37519] clean up warnings

* [JENKINS-37519] add logic to CapabilityApi to strip out duplicates

* [JENKINS-37519] enable the parsing of 'actions' by default so behavior is consistent (per kzantow) and because performance impact appears minimal

* [JENKINS-37519] don't log warnings by default; turn off perf metrics in the test suite

* JENKINS-38136# capability map API changed to POST

- Also pulls in well known capabilities in to a class as constant
- Fixes typo in PullRequest capability

* Doc update

* [JENKINS-37519] use POST for capabilities fetch, since a very long class list could break for a GET

* [JENKINS-37519] expose "capable" as static func so it can be used in place of "can" instance func that will get lost during clones

* tick version

* [JENKINS-37519] remove decoration of data with "can" method since it's easy to lose during a clone; update test to use "capable" util instead

* [JENKINS-37519] delint; jsdoc cleanup; fix test spec name

* tick blueocean-core-js version after publishing 0.0.15
This commit is contained in:
Cliff Meyers 2016-09-20 10:21:54 -04:00 committed by GitHub
parent cd83f83a3f
commit aee80261d8
16 changed files with 5592 additions and 4 deletions

View File

@ -1,6 +1,6 @@
{
"name": "@jenkins-cd/blueocean-core-js",
"version": "0.0.14",
"version": "0.0.15",
"description": "Shared JavaScript libraries for use with Jenkins Blue Ocean",
"main": "dist/js/index.js",
"scripts": {
@ -52,8 +52,8 @@
"eslint-plugin-react": "^5.0.1",
"gulp": "3.9.1",
"gulp-babel": "6.1.2",
"gulp-copy": "0.0.2",
"gulp-clean": "0.3.2",
"gulp-copy": "0.0.2",
"gulp-eslint": "2.0.0",
"gulp-less": "3.1.0",
"gulp-rename": "1.2.2",
@ -71,6 +71,7 @@
"react-addons-test-utils": "15.1.0",
"react-dom": "15.1.0",
"run-sequence": "1.2.1",
"sinon": "1.17.5",
"watchify": "3.7.0"
}
}

View File

@ -0,0 +1,35 @@
/**
* Created by cmeyers on 9/8/16.
*/
import { Fetch } from '../fetch';
import config from '../urlconfig';
import utils from '../utils';
export class CapabilityApi {
/**
* Fetch the capabilities for one or more class names.
*
* @param {Array} classNames
* @returns {Promise} with fulfilled {object} keyed by className, with an array of string capability names.
* @private
*/
fetchCapabilities(classNames) {
const noDuplicates = classNames.filter((item, index, self) => self.indexOf(item) === index);
const path = config.getJenkinsRootURL();
const classesUrl = utils.cleanSlashes(`${path}/blue/rest/classes/`);
const fetchOptions = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(
{ q: noDuplicates }
),
};
return Fetch.fetchJSON(classesUrl, { fetchOptions });
}
}

View File

@ -0,0 +1,149 @@
/**
* Created by cmeyers on 9/8/16.
*/
const addClass = (clazz, classMap) => {
const className = clazz._class;
if (!classMap[className]) {
// eslint-disable-next-line no-param-reassign
classMap[className] = [];
}
classMap[className].push(clazz);
};
const canWalk = (item) => item && (typeof item === 'object' || Array.isArray(item));
const DEFAULT_IGNORED_PROPS = ['_links'];
/**
* Decorate an object graph with a '_capabilities' property for each object with a valid '_class'
* Usage:
* import { capabilityAugmenter } from '@jenkins-cd/blueocean-core-js';
* const augmentCapability = capabilityAugmenter.augmentCapability;
*
* fetch(url, fetchOptions)
* .then(data => augmentCapability(data));
*/
export class CapabilityAugmenter {
constructor(capabilityStore) {
this._capabilityStore = capabilityStore;
this._perfLoggingEnabled = false;
this._warnLoggingEnabled = false;
}
/**
* Add "_capabilities" data or all objects with a "_class" property.
*
* @param {object|Array} data
* @returns {Promise}
*/
augmentCapabilities(data) {
const classMap = this._findClassesInTree(data);
return this._resolveCapabilities(data, classMap);
}
enablePerfLogging() {
this._perfLoggingEnabled = true;
}
enableWarningLogging() {
this._warnLoggingEnabled = true;
}
/**
* Find all of the distinct "_class" values in supplied object.
*
* @param {object|Array} data
* @returns {object} key= "_class" name, value= array of all objects of that class.
* @private
*/
_findClassesInTree(data) {
const classMap = {};
const nodesToWalk = [data];
const nodesAlreadyWalked = [];
const ignoredProps = DEFAULT_IGNORED_PROPS.slice();
const started = new Date().getTime();
let node = nodesToWalk.shift();
while (node) {
nodesAlreadyWalked.push(node);
// save a ref to the class so we can attach capabilities later
if (typeof node === 'object' && node._class) {
addClass(node, classMap);
}
const nodeKeys = Object.keys(node);
for (const key of nodeKeys) {
const value = node[key];
// walk this node at a later iteration as long as
// - we didn't already walk it (cycles cause an infinite loop otherwise)
// - the property name isn't on the blacklist
if (canWalk(value) && nodesAlreadyWalked.indexOf(value) === -1 && ignoredProps.indexOf(key) === -1) {
nodesToWalk.push(value);
}
}
node = nodesToWalk.shift();
}
if (this._perfLoggingEnabled) {
console.debug(`augmenter.parse: ${new Date().getTime() - started}ms`);
}
return classMap;
}
_resolveCapabilities(data, classMap) {
const classNames = Object.keys(classMap);
return this._capabilityStore.resolveCapabilities(...classNames)
.then(capabilitiesMap => this._injectCapabilities(classMap, capabilitiesMap))
.then(() => data);
}
/**
* Add the capabilities to the "_capabilities" property of all objects in the class map.
*
* @param classMap
* @param capabilitiesMap
* @returns {object} classMap
* @private
*/
_injectCapabilities(classMap, capabilitiesMap) {
const started = new Date().getTime();
const unresolved = [];
for (const className of Object.keys(classMap)) {
for (const target of classMap[className]) {
const capabilities = capabilitiesMap[className];
if (!capabilities && unresolved.indexOf(className) === -1) {
unresolved.push(className);
}
target._capabilities = capabilities || [];
}
}
if (this._perfLoggingEnabled) {
console.debug(`augmenter.inject: ${new Date().getTime() - started}ms`);
}
if (this._warnLoggingEnabled) {
for (const className of unresolved) {
console.warn(`could not resolve capabilities for ${className}; an error may have occurred during lookup`);
}
}
return classMap;
}
}

View File

@ -0,0 +1,76 @@
/**
* Created by cmeyers on 8/31/16.
*/
import es6Promise from 'es6-promise'; es6Promise.polyfill();
/**
* Retrieves capability metadata for class names.
* Uses an internal cache to minimize REST API calls.
*/
export class CapabilityStore {
constructor(capabilityApi) {
this._store = {};
this._capabilityApi = capabilityApi;
}
/**
* Fetch the capabilities for one or more class names.
* Will used cached values if available.
*
* @param classNames
* @returns {Promise} with fulfilled {object} keyed by className, with an array of string capability names.
*/
resolveCapabilities(...classNames) {
const result = {};
const classesToFetch = [];
// determine which class names are already in the cache and which aren't
for (const className of classNames) {
if (this._store[className]) {
result[className] = this._store[className];
} else {
classesToFetch.push(className);
}
}
// if nothing to fetch, just return an immediately fulfilled Promise
if (classesToFetch.length === 0) {
return new Promise(resolve => resolve(result));
}
// fetch the capabilities and then merge that with the values already in the cache
return this._fetchCapabilities(classesToFetch)
.then(fetchedCapabilities => Object.assign(result, fetchedCapabilities));
}
/**
* Fetch the capabilities for one or more class names.
*
* @param classNames
* @returns {Promise} with fulfilled {object} keyed by className, with an array of string capability names.
* @private
*/
_fetchCapabilities(classNames) {
return this._capabilityApi.fetchCapabilities(classNames)
.then(data => this._storeCapabilities(data.map));
}
/**
* Store the values in the cache and return it.
*
* @param map
* @returns {object} keyed by className, with an array of string capability names.
* @private
*/
_storeCapabilities(map) {
const storedCapabilities = {};
Object.keys(map).forEach(className => {
const capabilities = map[className];
this._store[className] = storedCapabilities[className] = capabilities.classes.slice();
});
return storedCapabilities;
}
}

View File

@ -0,0 +1,50 @@
/**
* Created by cmeyers on 9/9/16.
*/
/**
* Determines whether the supplied object has the supplied capability.
* As capabilities are typically name-spaced, this method will match on long or short names, e.g.
* if _capabilities = ['a.b.Capability1']
* passing either 'a.b.Capability1' or 'Capability1' will match
*
* @param {object} subject
* @param {string} capabilityName
* @returns {boolean}
*/
export const capable = (subject, capabilityName) => {
if (subject._capabilities) {
if (subject._capabilities.indexOf(capabilityName) !== -1) {
return true;
}
for (const capability of subject._capabilities) {
const shortName = capability.split('.').slice(-1).join('');
if (shortName === capabilityName) {
return true;
}
}
}
return false;
};
/**
* Provides some convenience methods for host object of _class/_capabilities
*/
export class Capable {
/**
* Determines whether the host object has the supplied capability.
* As capabilities are typically name-spaced, this method will match on long or short names, e.g.
* if _capabilities = ['a.b.Capability1']
* passing either 'a.b.Capability1' or 'Capability1' will match
*
* @param {string} capabilityName
* @returns {boolean}
*/
can(capabilityName) {
return capable(this, capabilityName);
}
}
export default new Capable();

View File

@ -0,0 +1,15 @@
/**
* Created by cmeyers on 9/8/16.
*/
import { CapabilityApi } from './CapabilityApi';
import { CapabilityStore } from './CapabilityStore';
import { CapabilityAugmenter } from './CapabilityAugmenter';
const api = new CapabilityApi();
const store = new CapabilityStore(api);
const augmenter = new CapabilityAugmenter(store);
// export as named singletons
export { store as capabilityStore };
export { augmenter as capabilityAugmenter };
export { capable } from './Capable';

View File

@ -20,6 +20,9 @@ export { RunButton } from './components/RunButton';
// export services as a singleton so all plugins will use the same instance
// capabilities
export { capable, capabilityStore, capabilityAugmenter } from './capability';
// limit to single instance so that duplicate REST calls aren't made as events come in
const sseBus = new SseBus(sse, Fetch.fetchJSON);
export { sseBus as SseBus };

View File

@ -5,6 +5,19 @@ const fetchJSON = Fetch.fetchJSON;
const fetch = Fetch.fetch;
export default {
/**
* Switches fetch functions with arbitrary replacements.
* Useful for test spies.
*
* @param _fetchJSON
* @param _fetch
*/
patchFetch(_fetchJSON, _fetch) {
Fetch.fetchJSON = _fetchJSON;
Fetch.fetch = _fetch;
},
/**
* Switches fetch functions for ones that dont use JWT. Needed
* for running tests.
@ -36,9 +49,9 @@ export default {
patchFetchWithData(dataFn) {
Fetch.fetchJSON = Fetch.fetch = (url, options) => {
const { onSuccess, onError } = options || {};
const data = Promise.resolve(dataFn(url, options));
if (onSuccess) {
return data.then(onSuccess).catch(FetchFunctions.onError(onError));
}

View File

@ -0,0 +1,44 @@
/**
* Created by cmeyers on 9/9/16.
*/
import { assert } from 'chai';
import sinon from 'sinon';
import testutils from '../../../src/js/testutils';
import { CapabilityApi } from '../../../src/js/capability/CapabilityApi';
const mock = {
fetch: () => {},
fetchJSON: () => {},
};
const fetch = sinon.spy(mock, 'fetch');
const fetchJSON = sinon.spy(mock, 'fetchJSON');
describe('CapabilityApi', () => {
let capabilityApi;
beforeEach(() => {
testutils.patchFetch(fetchJSON, fetch);
capabilityApi = new CapabilityApi();
});
afterEach(() => {
testutils.restoreFetch();
});
describe('fetchCapabilities', () => {
it('de-duplicates class names', () => {
capabilityApi.fetchCapabilities(['A', 'A', 'B']);
assert.isTrue(fetchJSON.calledOnce);
const fetchParams = fetchJSON.args[0][1];
assert.isOk(fetchParams);
const { fetchOptions } = fetchParams;
assert.isOk(fetchOptions);
const { body } = fetchOptions;
assert.isOk(body);
assert.equal(body, '{"q":["A","B"]}');
});
});
});

View File

@ -0,0 +1,217 @@
/**
* Created by cmeyers on 9/8/16.
*/
import es6Promise from 'es6-promise'; es6Promise.polyfill();
import { assert } from 'chai';
import { CapabilityAugmenter } from '../../../src/js/capability/CapabilityAugmenter';
import { capable } from '../../../src/js/capability/Capable';
class MockCapabilityStore {
constructor() {
this.classMap = {};
}
resolveCapabilities(... classNames) {
const result = {};
for (const className of classNames) {
result[className] = this.classMap[className];
}
return new Promise(resolve => resolve(result));
}
addCapability(className, ... capabilities) {
this.classMap[className] = [className].concat(capabilities);
}
}
describe('CapabilityAugmenter', () => {
let mockCapabilityStore;
let augmenter;
beforeEach(() => {
mockCapabilityStore = new MockCapabilityStore();
augmenter = new CapabilityAugmenter(mockCapabilityStore);
});
describe('augmentCapabilities', () => {
it('adds capabilities to a single multibranch pipeline', (done) => {
mockCapabilityStore.addCapability(
'io.jenkins.blueocean.rest.impl.pipeline.MultiBranchPipelineImpl',
'jenkins.branch.MultiBranchProject'
);
const multibranch = require('./multibranch-1.json');
augmenter.augmentCapabilities(multibranch)
.then(data => {
assert.isOk(data._capabilities);
assert.equal(data._capabilities.length, 2);
done();
});
});
it('adds capabilities to two branches from a multibranch pipeline', (done) => {
mockCapabilityStore.addCapability(
'io.jenkins.blueocean.rest.impl.pipeline.BranchImpl',
'io.jenkins.blueocean.rest.model.BlueBranch', 'org.jenkinsci.plugins.workflow.job.WorkflowJob',
'io.jenkins.blueocean.rest.impl.pipeline.PullRequest',
);
mockCapabilityStore.addCapability(
'io.jenkins.blueocean.rest.impl.pipeline.PipelineRunImpl',
'org.jenkinsci.plugins.workflow.job.WorkflowRun'
);
mockCapabilityStore.addCapability(
'io.jenkins.blueocean.service.embedded.rest.AbstractRunImpl$1'
);
const branches = require('./branches-1.json');
augmenter.augmentCapabilities(branches)
.then(data => {
assert.equal(data.length, 2);
const branch1 = data[0];
assert.isOk(branch1._capabilities);
assert.equal(branch1._capabilities.length, 4);
assert.isOk(branch1.latestRun._capabilities);
assert.equal(branch1.latestRun._capabilities.length, 2);
assert.isOk(branch1.latestRun.artifacts);
assert.isOk(branch1.latestRun.artifacts[0]._capabilities);
assert.equal(branch1.latestRun.artifacts[0]._capabilities.length, 1);
const branch2 = data[0];
assert.isOk(branch2._capabilities);
assert.equal(branch2._capabilities.length, 4);
assert.isOk(branch2.latestRun._capabilities);
assert.equal(branch2.latestRun._capabilities.length, 2);
assert.isOk(branch2.latestRun.artifacts);
assert.isOk(branch2.latestRun.artifacts[0]._capabilities);
assert.equal(branch2.latestRun.artifacts[0]._capabilities.length, 1);
done();
});
});
it('makes the "can" convenience method available', (done) => {
mockCapabilityStore.addCapability(
'io.jenkins.blueocean.rest.impl.pipeline.MultiBranchPipelineImpl',
'jenkins.branch.MultiBranchProject'
);
const multibranch = require('./multibranch-1.json');
augmenter.augmentCapabilities(multibranch)
.then(data => {
assert.isTrue(capable(data, 'jenkins.branch.MultiBranchProject'));
assert.isFalse(capable(data, 'jenkins.not.real.Capability'));
done();
});
});
it('initializes an empty array for an unknown capability', (done) => {
const unknown = { _class: 'foo.bar.Unknown' };
augmenter.augmentCapabilities(unknown)
.then(data => {
assert.isOk(data._capabilities);
assert.equal(data._capabilities.length, 0);
done();
});
});
});
describe('_findClassesInTree', () => {
it('builds the correct map for pipelines list', () => {
const pipelines = require('./pipelines-1.json');
const classMap = augmenter._findClassesInTree(pipelines);
assert.equal(Object.keys(classMap).length, 31);
const matrix = classMap['io.jenkins.blueocean.rest.impl.pipeline.MatrixProjectImpl'];
assert.isOk(matrix);
assert.equal(matrix.length, 1);
const multibranch = classMap['io.jenkins.blueocean.rest.impl.pipeline.MultiBranchPipelineImpl'];
assert.isOk(multibranch);
assert.equal(multibranch.length, 3);
const pipeline = classMap['io.jenkins.blueocean.rest.impl.pipeline.PipelineImpl'];
assert.isOk(pipeline);
assert.equal(pipeline.length, 4);
const pipelineRun = classMap['io.jenkins.blueocean.rest.impl.pipeline.PipelineRunImpl'];
assert.isOk(pipelineRun);
assert.equal(pipelineRun.length, 3);
const abstractPipeline = classMap['io.jenkins.blueocean.service.embedded.rest.AbstractPipelineImpl'];
assert.isOk(abstractPipeline);
assert.equal(abstractPipeline.length, 4);
const freestyleRun = classMap['io.jenkins.blueocean.service.embedded.rest.FreeStyleRunImpl'];
assert.isOk(freestyleRun);
assert.equal(freestyleRun.length, 4);
});
it('builds the correct map for favorites list', () => {
const favorites = require('./favorites-1.json');
const classMap = augmenter._findClassesInTree(favorites);
assert.equal(Object.keys(classMap).length, 42);
const branch = classMap['io.jenkins.blueocean.rest.impl.pipeline.BranchImpl'];
assert.isOk(branch);
assert.equal(branch.length, 4);
const pipeline = classMap['io.jenkins.blueocean.rest.impl.pipeline.PipelineImpl'];
assert.isOk(pipeline);
assert.equal(pipeline.length, 3);
const pipelineRun = classMap['io.jenkins.blueocean.rest.impl.pipeline.PipelineRunImpl'];
assert.isOk(pipelineRun);
assert.equal(pipelineRun.length, 6);
const abstractPipeline = classMap['io.jenkins.blueocean.service.embedded.rest.AbstractPipelineImpl'];
assert.isOk(abstractPipeline);
assert.equal(abstractPipeline.length, 4);
const abstractRun = classMap['io.jenkins.blueocean.service.embedded.rest.AbstractRunImpl$1'];
assert.isOk(abstractRun);
assert.equal(abstractRun.length, 1);
const favorite = classMap['io.jenkins.blueocean.service.embedded.rest.FavoriteImpl'];
assert.isOk(favorite);
assert.equal(favorite.length, 11);
const freestyleRun = classMap['io.jenkins.blueocean.service.embedded.rest.FreeStyleRunImpl'];
assert.isOk(freestyleRun);
assert.equal(freestyleRun.length, 4);
});
it('builds the correct map for a multibranch pipeline', () => {
const multibranch = require('./multibranch-1.json');
const classMap = augmenter._findClassesInTree(multibranch);
assert.equal(Object.keys(classMap).length, 5);
const multibranch1 = classMap['io.jenkins.blueocean.rest.impl.pipeline.MultiBranchPipelineImpl'];
assert.isOk(multibranch1);
assert.equal(multibranch1.length, 1);
// check the 'actions' capabilities too
// eslint-disable-next-line max-len
const action1 = classMap['com.cloudbees.hudson.plugins.folder.properties.FolderCredentialsProvider$FolderCredentialsProperty$CredentialsStoreActionImpl'];
assert.isOk(action1);
assert.equal(action1.length, 1);
const action2 = classMap['com.cloudbees.hudson.plugins.folder.relocate.RelocationAction'];
assert.isOk(action2);
assert.equal(action2.length, 1);
const action3 = classMap['com.cloudbees.plugins.credentials.ViewCredentialsAction'];
assert.isOk(action3);
assert.equal(action3.length, 1);
const action4 = classMap['org.jenkinsci.plugins.workflow.cps.Snippetizer$LocalAction'];
assert.isOk(action4);
assert.equal(action4.length, 1);
});
it('handles cycles in the data', () => {
const root = { _class: 'foo.Bar' };
root.cycle = root;
const classMap = augmenter._findClassesInTree(root);
assert.equal(Object.keys(classMap).length, 1);
const bar = classMap['foo.Bar'];
assert.isOk(bar);
assert.equal(bar.length, 1);
});
});
});

View File

@ -0,0 +1,87 @@
/**
* Created by cmeyers on 9/8/16.
*/
import { assert } from 'chai';
import es6Promise from 'es6-promise'; es6Promise.polyfill();
import sinon from 'sinon';
import { CapabilityStore } from '../../../src/js/capability/CapabilityStore';
const mockCapabilityApi = {
fetchCapabilities: (classNames) => {
const data = {
map: {},
};
if (classNames.indexOf('A') !== -1) {
data.map.A = { classes: ['A', 'A_one', 'A_two'] };
}
if (classNames.indexOf('B') !== -1) {
data.map.B = { classes: ['B', 'B_one', 'B_two'] };
}
return new Promise(resolve => resolve(data));
},
};
const fetchCapabilitiesSpy = sinon.spy(mockCapabilityApi, 'fetchCapabilities');
describe('CapabilityStore', () => {
let capabilityStore;
beforeEach(() => {
capabilityStore = new CapabilityStore(mockCapabilityApi);
fetchCapabilitiesSpy.reset();
});
it('resolves capabilities for a single class', (done) => {
capabilityStore.resolveCapabilities('A')
.then((capabilities) => {
assert.equal(Object.keys(capabilities).length, 1);
assert.isOk(capabilities.A);
assert.equal(capabilities.A.length, 3);
assert.equal(capabilities.A[0], 'A');
assert.equal(capabilities.A[1], 'A_one');
assert.equal(capabilities.A[2], 'A_two');
done();
});
});
it('resolves capabilities for multiple classes', (done) => {
capabilityStore.resolveCapabilities('A', 'B')
.then((capabilities) => {
assert.equal(Object.keys(capabilities).length, 2);
assert.isOk(capabilities.A);
assert.equal(capabilities.A.length, 3);
assert.isOk(capabilities.B);
assert.equal(capabilities.B.length, 3);
assert.equal(capabilities.B[0], 'B');
assert.equal(capabilities.B[1], 'B_one');
assert.equal(capabilities.B[2], 'B_two');
done();
});
});
it('only fetches from the API once when resolving the same class', (done) => {
capabilityStore.resolveCapabilities('A')
.then(() => {
capabilityStore.resolveCapabilities('A')
.then(() => {
assert.isOk(fetchCapabilitiesSpy.calledOnce);
done();
});
});
});
it('fetches from the API twice when resolving different classes', (done) => {
capabilityStore.resolveCapabilities('A')
.then(() => {
capabilityStore.resolveCapabilities('B')
.then(() => {
assert.isOk(fetchCapabilitiesSpy.calledTwice);
done();
});
});
});
});

View File

@ -0,0 +1,48 @@
/**
* Created by cmeyers on 9/9/16.
*/
import { assert } from 'chai';
import { Capable, capable } from '../../../src/js/capability/Capable';
describe('Capable', () => {
let testObj;
beforeEach(() => {
testObj = {
_capabilities: [
'a.b.LongName',
'ShortName',
],
};
testObj.can = new Capable().can;
});
describe('capable - static function', () => {
it('matches on long name (exact)', () => {
assert.isTrue(capable(testObj, 'a.b.LongName'));
});
it('matches on long name (using shortname)', () => {
assert.isTrue(capable(testObj, 'LongName'));
});
it('fails to match', () => {
assert.isFalse(capable(testObj, 'a.LongName'));
});
});
describe('can - embedded function', () => {
it('matches on long name (exact)', () => {
assert.isTrue(testObj.can('a.b.LongName'));
});
it('matches on long name (using shortname)', () => {
assert.isTrue(testObj.can('LongName'));
});
it('fails to match', () => {
assert.isFalse(testObj.can('a.LongName'));
});
});
});

View File

@ -0,0 +1,572 @@
[{
"_class": "io.jenkins.blueocean.rest.impl.pipeline.BranchImpl",
"_links": {
"activities": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/multibranch/pipelines/nested-folder/pipelines/jenkinsfile-experiments/branches/master/activities/"
},
"self": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/multibranch/pipelines/nested-folder/pipelines/jenkinsfile-experiments/branches/master/"
},
"actions": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/multibranch/pipelines/nested-folder/pipelines/jenkinsfile-experiments/branches/master/actions/"
},
"runs": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/multibranch/pipelines/nested-folder/pipelines/jenkinsfile-experiments/branches/master/runs/"
},
"queue": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/multibranch/pipelines/nested-folder/pipelines/jenkinsfile-experiments/branches/master/queue/"
}
},
"actions": [{
"_class": "io.jenkins.blueocean.service.embedded.rest.ActionProxiesImpl",
"_links": {
"self": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/multibranch/pipelines/nested-folder/pipelines/jenkinsfile-experiments/branches/master/move/"
}
},
"_class": "com.cloudbees.hudson.plugins.folder.relocate.RelocationAction",
"urlName": "move"
}, {
"_class": "io.jenkins.blueocean.service.embedded.rest.ActionProxiesImpl",
"_links": {
"self": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/multibranch/pipelines/nested-folder/pipelines/jenkinsfile-experiments/branches/master/wfapi/"
}
},
"_class": "com.cloudbees.workflow.rest.endpoints.JobAPI",
"urlName": "wfapi"
}, {
"_class": "io.jenkins.blueocean.service.embedded.rest.ActionProxiesImpl",
"_links": {
"self": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/multibranch/pipelines/nested-folder/pipelines/jenkinsfile-experiments/branches/master/workflow-stage/"
}
},
"_class": "com.cloudbees.workflow.ui.view.WorkflowStageViewAction",
"urlName": "workflow-stage"
}, {
"_class": "io.jenkins.blueocean.service.embedded.rest.ActionProxiesImpl",
"_links": {
"self": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/multibranch/pipelines/nested-folder/pipelines/jenkinsfile-experiments/branches/master/pipeline-syntax/"
}
},
"_class": "org.jenkinsci.plugins.workflow.cps.Snippetizer$LocalAction",
"urlName": "pipeline-syntax"
}, {
"_class": "io.jenkins.blueocean.service.embedded.rest.ActionProxiesImpl",
"_links": {
"self": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/multibranch/pipelines/nested-folder/pipelines/jenkinsfile-experiments/branches/master/credentials/"
}
},
"_class": "com.cloudbees.plugins.credentials.ViewCredentialsAction",
"stores": {},
"urlName": "credentials"
}],
"displayName": "master",
"estimatedDurationInMillis": 100690,
"fullName": "multibranch/nested-folder/jenkinsfile-experiments/master",
"lastSuccessfulRun": "http://localhost:8080/jenkins/blue/rest/organizations/jenkins/pipelines/multibranch/pipelines/nested-folder/pipelines/jenkinsfile-experiments/branches/master/runs/97/",
"latestRun": {
"_class": "io.jenkins.blueocean.rest.impl.pipeline.PipelineRunImpl",
"_links": {
"nodes": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/multibranch/pipelines/nested-folder/pipelines/jenkinsfile-experiments/branches/master/runs/97/nodes/"
},
"log": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/multibranch/pipelines/nested-folder/pipelines/jenkinsfile-experiments/branches/master/runs/97/log/"
},
"self": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/multibranch/pipelines/nested-folder/pipelines/jenkinsfile-experiments/branches/master/runs/97/"
},
"actions": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/multibranch/pipelines/nested-folder/pipelines/jenkinsfile-experiments/branches/master/runs/97/actions/"
},
"steps": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/multibranch/pipelines/nested-folder/pipelines/jenkinsfile-experiments/branches/master/runs/97/steps/"
}
},
"actions": [{
"_class": "io.jenkins.blueocean.service.embedded.rest.ActionProxiesImpl",
"_links": {
"self": null
},
"_class": "org.jenkinsci.plugins.workflow.cps.replay.ReplayFlowFactoryAction",
"urlName": null
}, {
"_class": "io.jenkins.blueocean.service.embedded.rest.ActionProxiesImpl",
"_links": {
"self": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/multibranch/pipelines/nested-folder/pipelines/jenkinsfile-experiments/branches/master/runs/97/cause/"
}
},
"_class": "hudson.model.CauseAction",
"causes": [{
"_class": "hudson.model.Cause$UserIdCause",
"shortDescription": "Started by user Cliff Meyers",
"userId": "cmeyers",
"userName": "Cliff Meyers"
}, {
"_class": "org.jenkinsci.plugins.workflow.cps.replay.ReplayCause",
"shortDescription": "Replayed #94"
}],
"urlName": "cause"
}, {
"_class": "io.jenkins.blueocean.service.embedded.rest.ActionProxiesImpl",
"_links": {
"self": null
},
"_class": "jenkins.scm.api.SCMRevisionAction",
"urlName": null
}, {
"_class": "io.jenkins.blueocean.service.embedded.rest.ActionProxiesImpl",
"_links": {
"self": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/multibranch/pipelines/nested-folder/pipelines/jenkinsfile-experiments/branches/master/runs/97/timings/"
}
},
"_class": "jenkins.metrics.impl.TimeInQueueAction",
"queuingDurationMillis": 2,
"totalDurationMillis": 98186,
"urlName": "timings"
}, {
"_class": "io.jenkins.blueocean.service.embedded.rest.ActionProxiesImpl",
"_links": {
"self": null
},
"_class": "jenkins.scm.api.SCMRevisionAction",
"urlName": null
}, {
"_class": "io.jenkins.blueocean.service.embedded.rest.ActionProxiesImpl",
"_links": {
"self": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/multibranch/pipelines/nested-folder/pipelines/jenkinsfile-experiments/branches/master/runs/97/wfapi/"
}
},
"_class": "com.cloudbees.workflow.rest.endpoints.RunAPI",
"urlName": "wfapi"
}, {
"_class": "io.jenkins.blueocean.service.embedded.rest.ActionProxiesImpl",
"_links": {
"self": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/multibranch/pipelines/nested-folder/pipelines/jenkinsfile-experiments/branches/master/runs/97/replay/"
}
},
"_class": "org.jenkinsci.plugins.workflow.cps.replay.ReplayAction",
"urlName": "replay"
}, {
"_class": "io.jenkins.blueocean.service.embedded.rest.ActionProxiesImpl",
"_links": {
"self": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/multibranch/pipelines/nested-folder/pipelines/jenkinsfile-experiments/branches/master/runs/97/flowGraph/"
}
},
"_class": "org.jenkinsci.plugins.workflow.job.views.FlowGraphAction",
"nodes": [{
"_class": "org.jenkinsci.plugins.workflow.graph.FlowStartNode"
}, {
"_class": "org.jenkinsci.plugins.workflow.cps.nodes.StepStartNode"
}, {
"_class": "org.jenkinsci.plugins.workflow.cps.nodes.StepStartNode"
}, {
"_class": "org.jenkinsci.plugins.workflow.cps.nodes.StepAtomNode"
}, {
"_class": "org.jenkinsci.plugins.workflow.cps.nodes.StepAtomNode"
}, {
"_class": "org.jenkinsci.plugins.workflow.cps.nodes.StepAtomNode"
}, {
"_class": "org.jenkinsci.plugins.workflow.cps.nodes.StepAtomNode"
}, {
"_class": "org.jenkinsci.plugins.workflow.cps.nodes.StepEndNode"
}, {
"_class": "org.jenkinsci.plugins.workflow.cps.nodes.StepEndNode"
}, {
"_class": "org.jenkinsci.plugins.workflow.graph.FlowEndNode"
}],
"urlName": "flowGraph"
}, {
"_class": "io.jenkins.blueocean.service.embedded.rest.ActionProxiesImpl",
"_links": {
"self": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/multibranch/pipelines/nested-folder/pipelines/jenkinsfile-experiments/branches/master/runs/97/flowGraphTable/"
}
},
"_class": "org.jenkinsci.plugins.workflow.job.views.FlowGraphTableAction",
"urlName": "flowGraphTable"
}, {
"_class": "io.jenkins.blueocean.service.embedded.rest.ActionProxiesImpl",
"_links": {
"self": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/multibranch/pipelines/nested-folder/pipelines/jenkinsfile-experiments/branches/master/runs/97/graphViz/"
}
},
"_class": "org.jenkinsci.plugins.workflow.job.views.GraphVizAction",
"urlName": "graphViz"
}],
"artifacts": [{
"_class": "io.jenkins.blueocean.service.embedded.rest.AbstractRunImpl$1",
"_links": {
"self": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/jenkins/job/multibranch/job/nested-folder/job/jenkinsfile-experiments/job/master/97/artifact/hey/"
}
},
"name": "hey",
"size": 4,
"url": "/jenkins/job/multibranch/job/nested-folder/job/jenkinsfile-experiments/job/master/97/artifact/hey"
}],
"changeSet": [],
"durationInMillis": 98184,
"enQueueTime": "2016-08-30T15:54:27.411-0400",
"endTime": "2016-08-30T15:56:05.596-0400",
"estimatedDurationInMillis": 100690,
"id": "97",
"organization": "jenkins",
"pipeline": "master",
"result": "SUCCESS",
"runSummary": "stable",
"startTime": "2016-08-30T15:54:27.412-0400",
"state": "FINISHED",
"type": "WorkflowRun",
"commitId": null
},
"name": "master",
"organization": "jenkins",
"permissions": {
"create": true,
"read": true,
"start": true,
"stop": true
},
"weatherScore": 100,
"pullRequest": null
}, {
"_class": "io.jenkins.blueocean.rest.impl.pipeline.BranchImpl",
"_links": {
"activities": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/multibranch/pipelines/nested-folder/pipelines/jenkinsfile-experiments/branches/test-branch-1/activities/"
},
"self": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/multibranch/pipelines/nested-folder/pipelines/jenkinsfile-experiments/branches/test-branch-1/"
},
"actions": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/multibranch/pipelines/nested-folder/pipelines/jenkinsfile-experiments/branches/test-branch-1/actions/"
},
"runs": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/multibranch/pipelines/nested-folder/pipelines/jenkinsfile-experiments/branches/test-branch-1/runs/"
},
"queue": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/multibranch/pipelines/nested-folder/pipelines/jenkinsfile-experiments/branches/test-branch-1/queue/"
}
},
"actions": [{
"_class": "io.jenkins.blueocean.service.embedded.rest.ActionProxiesImpl",
"_links": {
"self": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/multibranch/pipelines/nested-folder/pipelines/jenkinsfile-experiments/branches/test-branch-1/move/"
}
},
"_class": "com.cloudbees.hudson.plugins.folder.relocate.RelocationAction",
"urlName": "move"
}, {
"_class": "io.jenkins.blueocean.service.embedded.rest.ActionProxiesImpl",
"_links": {
"self": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/multibranch/pipelines/nested-folder/pipelines/jenkinsfile-experiments/branches/test-branch-1/wfapi/"
}
},
"_class": "com.cloudbees.workflow.rest.endpoints.JobAPI",
"urlName": "wfapi"
}, {
"_class": "io.jenkins.blueocean.service.embedded.rest.ActionProxiesImpl",
"_links": {
"self": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/multibranch/pipelines/nested-folder/pipelines/jenkinsfile-experiments/branches/test-branch-1/workflow-stage/"
}
},
"_class": "com.cloudbees.workflow.ui.view.WorkflowStageViewAction",
"urlName": "workflow-stage"
}, {
"_class": "io.jenkins.blueocean.service.embedded.rest.ActionProxiesImpl",
"_links": {
"self": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/multibranch/pipelines/nested-folder/pipelines/jenkinsfile-experiments/branches/test-branch-1/pipeline-syntax/"
}
},
"_class": "org.jenkinsci.plugins.workflow.cps.Snippetizer$LocalAction",
"urlName": "pipeline-syntax"
}, {
"_class": "io.jenkins.blueocean.service.embedded.rest.ActionProxiesImpl",
"_links": {
"self": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/multibranch/pipelines/nested-folder/pipelines/jenkinsfile-experiments/branches/test-branch-1/credentials/"
}
},
"_class": "com.cloudbees.plugins.credentials.ViewCredentialsAction",
"stores": {},
"urlName": "credentials"
}],
"displayName": "test-branch-1",
"estimatedDurationInMillis": 103741,
"fullName": "multibranch/nested-folder/jenkinsfile-experiments/test-branch-1",
"lastSuccessfulRun": "http://localhost:8080/jenkins/blue/rest/organizations/jenkins/pipelines/multibranch/pipelines/nested-folder/pipelines/jenkinsfile-experiments/branches/test-branch-1/runs/54/",
"latestRun": {
"_class": "io.jenkins.blueocean.rest.impl.pipeline.PipelineRunImpl",
"_links": {
"nodes": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/multibranch/pipelines/nested-folder/pipelines/jenkinsfile-experiments/branches/test-branch-1/runs/54/nodes/"
},
"log": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/multibranch/pipelines/nested-folder/pipelines/jenkinsfile-experiments/branches/test-branch-1/runs/54/log/"
},
"self": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/multibranch/pipelines/nested-folder/pipelines/jenkinsfile-experiments/branches/test-branch-1/runs/54/"
},
"actions": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/multibranch/pipelines/nested-folder/pipelines/jenkinsfile-experiments/branches/test-branch-1/runs/54/actions/"
},
"steps": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/multibranch/pipelines/nested-folder/pipelines/jenkinsfile-experiments/branches/test-branch-1/runs/54/steps/"
}
},
"actions": [{
"_class": "io.jenkins.blueocean.service.embedded.rest.ActionProxiesImpl",
"_links": {
"self": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/multibranch/pipelines/nested-folder/pipelines/jenkinsfile-experiments/branches/test-branch-1/runs/54/cause/"
}
},
"_class": "hudson.model.CauseAction",
"causes": [{
"_class": "hudson.model.Cause$UserIdCause",
"shortDescription": "Started by user Cliff Meyers",
"userId": "cmeyers",
"userName": "Cliff Meyers"
}],
"urlName": "cause"
}, {
"_class": "io.jenkins.blueocean.service.embedded.rest.ActionProxiesImpl",
"_links": {
"self": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/multibranch/pipelines/nested-folder/pipelines/jenkinsfile-experiments/branches/test-branch-1/runs/54/timings/"
}
},
"_class": "jenkins.metrics.impl.TimeInQueueAction",
"queuingDurationMillis": 2,
"totalDurationMillis": 98868,
"urlName": "timings"
}, {
"_class": "io.jenkins.blueocean.service.embedded.rest.ActionProxiesImpl",
"_links": {
"self": null
},
"_class": "jenkins.scm.api.SCMRevisionAction",
"urlName": null
}, {
"_class": "io.jenkins.blueocean.service.embedded.rest.ActionProxiesImpl",
"_links": {
"self": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/multibranch/pipelines/nested-folder/pipelines/jenkinsfile-experiments/branches/test-branch-1/runs/54/git/"
}
},
"_class": "hudson.plugins.git.util.BuildData",
"buildsByBranchName": {
"test-branch-1": {
"_class": "hudson.plugins.git.util.Build",
"buildNumber": 54,
"buildResult": null,
"marked": {
"SHA1": "b899164aee309155703bdb576c82730ca5fe2eb8",
"branch": [{
"SHA1": "b899164aee309155703bdb576c82730ca5fe2eb8",
"name": "test-branch-1"
}]
},
"revision": {
"SHA1": "b899164aee309155703bdb576c82730ca5fe2eb8",
"branch": [{
"SHA1": "b899164aee309155703bdb576c82730ca5fe2eb8",
"name": "test-branch-1"
}]
}
}
},
"lastBuiltRevision": {
"SHA1": "b899164aee309155703bdb576c82730ca5fe2eb8",
"branch": [{
"SHA1": "b899164aee309155703bdb576c82730ca5fe2eb8",
"name": "test-branch-1"
}]
},
"remoteUrls": ["https://github.com/cliffmeyers/jenkinsfile-experiments.git"],
"scmName": "",
"urlName": "git"
}, {
"_class": "io.jenkins.blueocean.service.embedded.rest.ActionProxiesImpl",
"_links": {
"self": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/multibranch/pipelines/nested-folder/pipelines/jenkinsfile-experiments/branches/test-branch-1/runs/54/tagBuild/"
}
},
"_class": "hudson.plugins.git.GitTagAction",
"tags": [],
"urlName": "tagBuild"
}, {
"_class": "io.jenkins.blueocean.service.embedded.rest.ActionProxiesImpl",
"_links": {
"self": null
},
"_class": "org.jenkinsci.plugins.workflow.steps.scm.MultiSCMRevisionState",
"urlName": null
}, {
"_class": "io.jenkins.blueocean.service.embedded.rest.ActionProxiesImpl",
"_links": {
"self": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/multibranch/pipelines/nested-folder/pipelines/jenkinsfile-experiments/branches/test-branch-1/runs/54/wfapi/"
}
},
"_class": "com.cloudbees.workflow.rest.endpoints.RunAPI",
"urlName": "wfapi"
}, {
"_class": "io.jenkins.blueocean.service.embedded.rest.ActionProxiesImpl",
"_links": {
"self": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/multibranch/pipelines/nested-folder/pipelines/jenkinsfile-experiments/branches/test-branch-1/runs/54/replay/"
}
},
"_class": "org.jenkinsci.plugins.workflow.cps.replay.ReplayAction",
"urlName": "replay"
}, {
"_class": "io.jenkins.blueocean.service.embedded.rest.ActionProxiesImpl",
"_links": {
"self": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/multibranch/pipelines/nested-folder/pipelines/jenkinsfile-experiments/branches/test-branch-1/runs/54/flowGraph/"
}
},
"_class": "org.jenkinsci.plugins.workflow.job.views.FlowGraphAction",
"nodes": [{
"_class": "org.jenkinsci.plugins.workflow.graph.FlowStartNode"
}, {
"_class": "org.jenkinsci.plugins.workflow.cps.nodes.StepStartNode"
}, {
"_class": "org.jenkinsci.plugins.workflow.cps.nodes.StepStartNode"
}, {
"_class": "org.jenkinsci.plugins.workflow.cps.nodes.StepAtomNode"
}, {
"_class": "org.jenkinsci.plugins.workflow.cps.nodes.StepAtomNode"
}, {
"_class": "org.jenkinsci.plugins.workflow.cps.nodes.StepAtomNode"
}, {
"_class": "org.jenkinsci.plugins.workflow.cps.nodes.StepAtomNode"
}, {
"_class": "org.jenkinsci.plugins.workflow.cps.nodes.StepEndNode"
}, {
"_class": "org.jenkinsci.plugins.workflow.cps.nodes.StepEndNode"
}, {
"_class": "org.jenkinsci.plugins.workflow.graph.FlowEndNode"
}],
"urlName": "flowGraph"
}, {
"_class": "io.jenkins.blueocean.service.embedded.rest.ActionProxiesImpl",
"_links": {
"self": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/multibranch/pipelines/nested-folder/pipelines/jenkinsfile-experiments/branches/test-branch-1/runs/54/flowGraphTable/"
}
},
"_class": "org.jenkinsci.plugins.workflow.job.views.FlowGraphTableAction",
"urlName": "flowGraphTable"
}, {
"_class": "io.jenkins.blueocean.service.embedded.rest.ActionProxiesImpl",
"_links": {
"self": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/multibranch/pipelines/nested-folder/pipelines/jenkinsfile-experiments/branches/test-branch-1/runs/54/graphViz/"
}
},
"_class": "org.jenkinsci.plugins.workflow.job.views.GraphVizAction",
"urlName": "graphViz"
}],
"artifacts": [{
"_class": "io.jenkins.blueocean.service.embedded.rest.AbstractRunImpl$1",
"_links": {
"self": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/jenkins/job/multibranch/job/nested-folder/job/jenkinsfile-experiments/job/test-branch-1/54/artifact/hey/"
}
},
"name": "hey",
"size": 4,
"url": "/jenkins/job/multibranch/job/nested-folder/job/jenkinsfile-experiments/job/test-branch-1/54/artifact/hey"
}],
"changeSet": [],
"durationInMillis": 98866,
"enQueueTime": "2016-08-31T20:10:22.406-0400",
"endTime": "2016-08-31T20:12:01.273-0400",
"estimatedDurationInMillis": 103741,
"id": "54",
"organization": "jenkins",
"pipeline": "test-branch-1",
"result": "SUCCESS",
"runSummary": "stable",
"startTime": "2016-08-31T20:10:22.407-0400",
"state": "FINISHED",
"type": "WorkflowRun",
"commitId": "b899164aee309155703bdb576c82730ca5fe2eb8"
},
"name": "test-branch-1",
"organization": "jenkins",
"permissions": {
"create": true,
"read": true,
"start": true,
"stop": true
},
"weatherScore": 100,
"pullRequest": null
}]

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,84 @@
{
"_class": "io.jenkins.blueocean.rest.impl.pipeline.MultiBranchPipelineImpl",
"_links": {
"activities": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/jdl1/activities/"
},
"self": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/jdl1/"
},
"branches": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/jdl1/branches/"
},
"actions": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/jdl1/actions/"
},
"runs": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/jdl1/runs/"
},
"queue": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/jdl1/queue/"
}
},
"actions": [{
"_class": "io.jenkins.blueocean.service.embedded.rest.ActionProxiesImpl",
"_links": {
"self": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/jdl1/move/"
}
},
"_class": "com.cloudbees.hudson.plugins.folder.relocate.RelocationAction",
"urlName": "move"
}, {
"_class": "io.jenkins.blueocean.service.embedded.rest.ActionProxiesImpl",
"_links": {
"self": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/jdl1/pipeline-syntax/"
}
},
"_class": "org.jenkinsci.plugins.workflow.cps.Snippetizer$LocalAction",
"urlName": "pipeline-syntax"
}, {
"_class": "io.jenkins.blueocean.service.embedded.rest.ActionProxiesImpl",
"_links": {
"self": {
"_class": "io.jenkins.blueocean.rest.hal.Link",
"href": "/blue/rest/organizations/jenkins/pipelines/jdl1/credentials/"
}
},
"_class": "com.cloudbees.plugins.credentials.ViewCredentialsAction",
"stores": {
"folder": {
"_class": "com.cloudbees.hudson.plugins.folder.properties.FolderCredentialsProvider$FolderCredentialsProperty$CredentialsStoreActionImpl"
}
},
"urlName": "credentials"
}],
"displayName": "jdl1",
"fullName": "jdl1",
"name": "jdl1",
"organization": "jenkins",
"permissions": {
"create": true,
"read": true
},
"estimatedDurationInMillis": 976,
"numberOfFolders": 0,
"numberOfPipelines": 3,
"weatherScore": 0,
"branchNames": ["master", "docker-test", "experiment%2Fbuild-locally-docker"],
"numberOfFailingBranches": 1,
"numberOfFailingPullRequests": 0,
"numberOfSuccessfulBranches": 1,
"numberOfSuccessfulPullRequests": 0,
"totalNumberOfBranches": 3,
"totalNumberOfPullRequests": 0
}

File diff suppressed because it is too large Load Diff