Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
parser: babel-eslint

parserOptions:
ecmaVersion": 6,
sourceType: module
Expand Down
90 changes: 90 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,96 @@ Install
$ npm install --save teamcity-build-queue
```

Usage
-----

```js
const queueInfo = require('teamcity-build-queue');

queueInfo('http://teamcity.domain.com', {
projectPattern: 'project :: Pull requests :: *',
ignoreDependencies: true, // ignore builds with dependencies that have not been built yet
ignoreIncompatibleAgents: true // ignore builds without compatible agents
})
.then(queue => {
console.log(queue.builds);
console.log(queue.size);
});
```

API
---

### queueInfo(url[, options])

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Это как-то автоматически генерируется?
Меня всегда смущало, что имена методов и параметров в такой доке пишутся не моноширинным шрифтом. Типа, таким образом иллюстрируя, что это код.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Я руками пишу.

И так понятно что это про код. А использовать двойные выделения (заголовок уже выделен) плохой тон.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Можно генерить чем-то вроде verb.


Returns a Promise, that resolves to object with builds from Build Queue.

#### url

Type: `string`

The URL to TeamCity host.

#### options

Type: `object`

#### options.projectPattern

Type: `string`

The pattern of project name to filter builds.

If pattern is not specified, then all builds will be in the result.

**Wildcards**

```js
queueInfo('http://teamcity.domain.com', {
projectPattern: 'project :: Pull requests :: *'

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

А как правильно написать, чтобы все подпроекты учитывались? Две звёздочки?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Одна звёздочка матчится на все подпроекты.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Понятно, давай может добавим такой случай в пример ниже?

});

// Will be taken into account builds the following assemblies:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"assemblies" какой-то не ТС термин. Projects? Configurations?


// project :: Pull requests :: build
// project :: Pull requests :: tests :: unit
// project :: Pull requests :: tests :: e2e
// project :: Pull requests :: docs
// project :: Pull requests :: deploy
// ...
```

**Brace Expansion**

```js
queueInfo('http://teamcity.domain.com', {
projectPattern: 'project :: {Pull requests, dev} :: *'
});

// Will be taken into account builds the following configurations:
//
// project :: Pull requests :: build
// project :: Pull requests :: tests
// ...
// project :: dev :: build
// project :: dev :: tests
// ...
```

Read more about it in [micromatch](https://github.com/jonschlinkert/micromatch#features) package.

#### options.ignoreDependencies

Type: `string` Default: `false`

To ignore builds with dependencies that have not been built yet.

#### options.ignoreIncompatibleAgents

Type: `string` Default: `false`

To ignore builds without compatible agents.

License
-------

Expand Down
43 changes: 43 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
'use strict';

const assert = require('assert');
const url = require('url');

const isUrl = require('is-url');
const got = require('got');

const load = require('./lib/load-build');
const filterBuilds = require('./lib/filter-builds');

const TEAMCITY_BUILD_QUEUE_PATHNAME = 'guestAuth/app/rest/buildQueue';

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Пугает guestAuth, особенно в контексте отказа от него.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Меня тоже :)

Давай обсудим голосом.


/**
* Loads info about TeamCity Build Queue.
*
* @param {string} teamcityUrl - the URL to TeamCity host.
* @param {object} options - the options.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

THE options!
Кажется, кэп.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Линтер обязывает

* @param {string} options.projectPattern - the pattern of project name to filter builds.
* @param {boolean} options.ignoreDependencies - to ignore builds with dependencies that have not been built yet.
* @param {boolean} options.ignoreIncompatibleAgents - to ignore builds without compatible agents.
* @returns {{builds: object[], size: number}}
*/
module.exports = (teamcityUrl, options) => {
assert(isUrl(teamcityUrl), 'You should specify url to TeamCity');

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

А я обычно юзаю console.assert. Есть какая-то разница?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Меня смущает, что поведение console.assert в браузерах другое. Чтобы не вносить путаницы использую assert.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Какое другое? Не знал.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


return got(url.resolve(teamcityUrl, TEAMCITY_BUILD_QUEUE_PATHNAME), { json: true })
.then(response => {
const body = response.body;
const builds = body && body.build || [];

return Promise.all(builds.map(build => {
const buildUrl = url.resolve(teamcityUrl, build.href);

return load(buildUrl);
}));
})
.then(builds => filterBuilds(builds, options))
.then(builds => ({
builds: builds,
size: builds.length
}));
};
74 changes: 74 additions & 0 deletions lib/filter-builds.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
const mm = require('micromatch');

/**
* Removes slashes.
*
* @param {string} str - some string.
* @returns {string}
*/
function clearSlash(str) {
return str.replace(/\//g, '');
}

/**
* Returns `true` if the project name of build match the project pattern.
*
* @param {object} build - the build info.
* @param {string} projectPattern - the pattern of project name to filter builds.
* @returns {boolean}
*/
function isMatchProjectName(build, projectPattern) {
const buildType = build.buildType;
const projectName = buildType && buildType.projectName;

return mm.isMatch(clearSlash(projectName), clearSlash(projectPattern), { nocase: true });
}

/**
* Returns `true` the build has dependency builds.
*
* @param {object} build - the build info.
* @returns {boolean}
*/
function hasDependencies(build) {
return build.waitReason === 'Build dependencies have not been built yet';
}

/**
* Returns `true` the build in queue and has compatible agents.
*
* @param {object} build - the build info.
* @returns {boolean}
*/
function isQueuedWithCompatibleAgents(build) {
const compatibleAgents = build.compatibleAgents;
const isQueued = build.state === 'queued';
const hasCompatibleAgents = compatibleAgents && compatibleAgents.length !== 0;

return isQueued ? hasCompatibleAgents : true;
}

/**
* Filters builds by projectPattern and ignores inappropriate builds.
*
* @param {object[]} builds - the objects with build info.
* @param {string} options - the options.
* @param {string} options.projectPattern - the pattern of project name to filter builds.
* @param {boolean} options.ignoreDependencies - to ignore builds with dependencies that have not been built yet.
* @param {boolean} options.ignoreIncompatibleAgents - to ignore builds without compatible agents.
* @returns {object[]}
*/
module.exports = (builds, options) => {
const projectPattern = options.projectPattern;

return builds.filter(build => {
if (projectPattern && !isMatchProjectName(build, projectPattern)
|| options.ignoreDependencies && hasDependencies(build)
|| options.ignoreIncompatibleAgents && !isQueuedWithCompatibleAgents(build)
) {
return false;
}

return true;
});
};
46 changes: 46 additions & 0 deletions lib/load-build.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
const got = require('got');

/**
* Loads info about compatible agents of build.
*
* @param {string} buildUrl - the relative url to build.
* @returns {object}
*/
function loadBuild(buildUrl) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Смущает, что сюда приходит ссылка, а не buildId, например. Что ссылка формируется где-то в другом месте. Размазывание логики, кмк. Но решай сам.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Одного buildId мало, можно целиком build передавать.

Но формирование урла зависит от переданного teamcityUrl.

return got(buildUrl, { json: true })
.then(response => response.body);
}

/**
* Loads info about compatible agents of build.
*
* @param {string} buildUrl - the relative url to build.
* @returns {object}
*/
function loadCompatibleAgents(buildUrl) {
const agentsUrl = `${buildUrl}/compatibleAgents`;

return got(agentsUrl, { json: true })
.then(response => response.body.agent)
.catch(() => ([])); // If build had time to start, compatible agents url returns error.
}

/**
* Loads info about build with it compatible agents.
*
* @param {string} buildUrl - the relative url to build.
* @returns {object}
*/
module.exports = (buildUrl) => {
return Promise.all([
loadBuild(buildUrl),
loadCompatibleAgents(buildUrl)
]).then(data => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

там нету spread?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Нет, в ES6 .spread() не нужен, т.к. есть spread оператор:

Promise.all([...]).then([build, agents] => {});

Но в 4й ноде его нет.

const build = data[0];
const compatibleAgents = data[1];

build.compatibleAgents = compatibleAgents;

return build;
});
};
17 changes: 15 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,30 @@
"engines": {
"node": ">= 4.0"
},
"dependencies": {},
"dependencies": {
"got": "6.3.0",
"is-url": "1.2.1",
"micromatch": "2.3.8"
},
"devDependencies": {
"ava": "^0.15.0",
"babel-eslint": "^6.0.4",
"coveralls": "^2.11.9",
"eslint": "^2.10.2",
"eslint-config-pedant": "^0.5.1",
"nyc": "^6.4.4"
"nyc": "^6.4.4",
"proxyquire": "^1.7.9",
"sinon": "^1.17.4",
"sinon-as-promised": "^4.0.0"
},
"scripts": {
"pretest": "eslint .",
"test": "nyc ava",
"coveralls": "nyc report --reporter=text-lcov | coveralls"
},
"ava": {
"require": [
"sinon-as-promised"
]
}
}
57 changes: 57 additions & 0 deletions test/api.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
'use strict';

const test = require('ava');
const proxyquire = require('proxyquire');
const sinon = require('sinon');

test.beforeEach(t => {
t.context.gotStub = sinon.stub().resolves({ body: {} });
t.context.loadStub = sinon.stub().resolves([]);
t.context.filterStub = sinon.stub().returns([]);

t.context.run = proxyquire('../index', {
got: t.context.gotStub,
'./lib/load-build': t.context.loadStub,
'./lib/filter-builds': t.context.filterStub
});
});

test('should get info by url', async t => {
await t.context.run('http://tc.url');

t.true(t.context.gotStub.calledWith('http://tc.url/guestAuth/app/rest/buildQueue'));
});

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Где же async/await? :)


test('should returns builds of queue', async t => {
const builds = [{ id: '1' }];

t.context.filterStub.returns(builds);

const queue = await t.context.run('http://tc.url');

t.deepEqual(queue.builds, builds);
});

test('should returns queue size', async t => {
const builds = [{ id: '1' }];

t.context.filterStub.returns(builds);

const queue = await t.context.run('http://tc.url');

t.is(queue.size, builds.length);
});

test('should load build info', async t => {
t.context.gotStub.resolves({
body: {
href: '/guestAuth/app/rest/buildQueue',
count: 1,
build: [{ href: 'build-url-1' }]
}
});

await t.context.run('http://tc.url');

t.true(t.context.loadStub.calledWith('http://tc.url/build-url-1'));
});
19 changes: 19 additions & 0 deletions test/errors.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
'use strict';

const test = require('ava');

const queueInfo = require('../index');

test('should throw error if url is not speсified', t => {
t.throws(
() => queueInfo(),
'You should specify url to TeamCity'
);
});

test('should throw error if url is not url', t => {
t.throws(
() => queueInfo('bla'),
'You should specify url to TeamCity'
);
});
Loading