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:
parent
cd83f83a3f
commit
aee80261d8
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
|
@ -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';
|
|
@ -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 };
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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"]}');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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'));
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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
|
@ -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
Loading…
Reference in New Issue