Skip to content

Commit

Permalink
Initial API
Browse files Browse the repository at this point in the history
  • Loading branch information
Jeff Jagoda committed Jul 6, 2018
0 parents commit 0b66fa1
Show file tree
Hide file tree
Showing 13 changed files with 4,985 additions and 0 deletions.
9 changes: 9 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[*]
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

[*.{js,json,yml}]
charset = utf-8
indent_size = 2
indent_style = space
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/.nyc_output/
/node_modules/
22 changes: 22 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
language: node_js
node_js:
- '8'
- '9'
- '10'
before_install: yarn global add greenkeeper-lockfile@1
before_script: greenkeeper-lockfile-update
script: yarn test
after_script: greenkeeper-lockfile-upload
after_success: yarn coverage
notifications:
email:
on_success: never
on_failure: always
deploy:
provider: npm
email: $NPM_EMAIL
api_key: $NPM_KEY
on:
tags: true
repo: lifeomic/bitrise
node: '8'
76 changes: 76 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
bitrise
=======

[![Build Status](https://travis-ci.org/lifeomic/bitrise.svg?branch=master)](https://travis-ci.org/lifeomic/bitrise)
[![Coverage Status](https://coveralls.io/repos/github/lifeomic/bitrise/badge.svg?branch=master)](https://coveralls.io/github/lifeomic/bitrise?branch=master)

A simple API client for the [Bitrise API][bitrise-api].

## Usage

$ npm install --save @lifeomic/bitrise

## API

### bitrise({ token })

Create a new client instance. `token` is a [personal access token][bitrise-auth].

const bitrise = require('@lifeomic/bitrise');
const client = bitrise({ token: 'some-token' });

### bitrise.app({ slug })

Create an app object. `slug` is the Bitrise app slug.

const bitrise = require('@lifeomic/bitrise');
const app = bitrise({ token }).app({ slug });

An app has the following attributes:

- **slug** — the app's unique identifier.

### async app.triggerBuild(options)

Trigger a new build for the app. Supported `options` include the following:

- **commitHash** — the hash of the commit to checkout of SCM. By default
the `master` branch is run.
- **workflow** — the ID of the Bitrise workflow to run.

Returns a `build` object representing the build that was started. A build has
the following attributes:

- **appSlug** — the slug of the application that the build is for.
- **buildSlug** — the unique ID of the build.

References:
- https://devcenter.bitrise.io/api/v0.1/#post-appsapp-slugbuilds

### async build.describe()

Get all attributes for a build.

References:
- https://devcenter.bitrise.io/api/v0.1/#post-appsapp-slugbuilds

### async build.follow()

Poll on the logs for a build and print them to stdout. An error will be thrown
if the build fails.

References:
- https://devcenter.bitrise.io/api/v0.1/#get-appsapp-slugbuildsbuild-sluglog
- https://devcenter.bitrise.io/api/v0.1/#get-appsapp-slugbuildsbuild-slug

### async build.isFinished()

Returns `true` if the build has completed execution (regardless of success or
failure). Returns `false` otherwise. This is just a convenience method for
running `build.describe()` and checking the `finished_at` attribute.

References:
- https://devcenter.bitrise.io/api/v0.1/#get-appsapp-slugbuildsbuild-slug

[bitrise-api]: https://devcenter.bitrise.io/api/v0.1/ "Bitrise API"
[bitrise-auth]: https://devcenter.bitrise.io/api/v0.1/#authentication "API Authorization"
45 changes: 45 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"name": "@lifeomic/bitrise",
"version": "0.0.0",
"description": "Bitrise API client",
"main": "src/client.js",
"repository": "https://github.com/lifeomic/bitrise.git",
"author": "LifeOmic <development@lifeomic.com>",
"license": "MIT",
"scripts": {
"coverage": "nyc report --reporter=text-lcov | coveralls",
"lint": "eslint .",
"pretest": "yarn lint",
"test": "nyc ava -v"
},
"dependencies": {
"axios": "^0.18.0"
},
"devDependencies": {
"@lifeomic/eslint-plugin-node": "^1.1.1",
"ava": "^0.25.0",
"coveralls": "^3.0.2",
"eslint": "^4.0.0",
"lodash": "^4.17.10",
"luxon": "^1.3.1",
"nyc": "^12.0.2",
"sinon": "^6.1.0",
"uuid": "^3.3.2"
},
"ava": {
"failWithoutAssertions": false,
"files": [
"test/**/*.test.js"
]
},
"eslintConfig": {
"extends": "plugin:@lifeomic/node/recommended"
},
"nyc": {
"branches": 100,
"check-coverage": true,
"functions": 100,
"lines": 100,
"statements": 100
}
}
35 changes: 35 additions & 0 deletions src/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
const build = require('./build');
const project = require('../package.json');

const buildParameters = ({ commitHash, workflow }) => {
const parameters = {};

if (commitHash) {
parameters.commit_hash = commitHash;
} else {
parameters.branch = 'master';
}

if (workflow) {
parameters.workflow_id = workflow;
}

return parameters;
};

const triggerBuild = async ({ client, slug }, options = {}) => {
const buildOptions = {
build_params: buildParameters(options),
hook_info: { type: 'bitrise' },
triggered_by: project.name
};

const response = await client.post(`/apps/${slug}/builds`, buildOptions);
return build({ appSlug: slug, client, buildSlug: response.data.build_slug });
};

module.exports = ({ client, slug }) => {
const app = { slug };
app.triggerBuild = triggerBuild.bind(app, { client, slug });
return app;
};
52 changes: 52 additions & 0 deletions src/build.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
const describeBuild = async ({ appSlug, buildSlug, client }) => {
const response = await client.get(`/apps/${appSlug}/builds/${buildSlug}`);
return response.data.data;
};

const followBuild = async ({ appSlug, buildSlug, client }) => {
let timestamp;

do {
if (timestamp) {
await sleep(5000);
}

const parameters = timestamp ? `?timestamp=${timestamp}` : '';
const response = await client.get(`/apps/${appSlug}/builds/${buildSlug}/log${parameters}`);

// If the log has already been archived then polling is no good. Just
// download and print the log data.
if (response.data.is_archived && !timestamp) {
const archiveResponse = await client.get(response.data.expiring_raw_log_url);
process.stdout.write(archiveResponse.data);
break;
}

response.data.log_chunks.forEach(({ chunk }) => process.stdout.write(chunk));
timestamp = response.data.timestamp;
} while (timestamp);

const attributes = await describeBuild({ appSlug, buildSlug, client });

if (attributes.status > 1) {
throw new Error(`Build ${appSlug}/${buildSlug} failed`);
}
};

const isFinished = async ({ appSlug, buildSlug, client }) => {
const attributes = await describeBuild({ appSlug, buildSlug, client });
return !!attributes.finished_at;
};

const sleep = (delay) => new Promise((resolve) => setTimeout(resolve, delay));

module.exports = ({ appSlug, buildSlug, client }) => {
const build = { appSlug, buildSlug };
const state = { appSlug, buildSlug, client };

build.describe = describeBuild.bind(build, state);
build.follow = followBuild.bind(build, state);
build.isFinished = isFinished.bind(build, state);

return build;
};
16 changes: 16 additions & 0 deletions src/client.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
const app = require('./app');
const assert = require('assert');
const axios = require('axios');

module.exports = ({ token }) => {
assert(token, 'An access token is required');

const client = axios.create({
baseURL: 'https://api.bitrise.io/v0.1',
headers: { Authorization: `token ${token}` }
});

const instance = {};
instance.app = ({ slug }) => app({ client, slug });
return instance;
};
83 changes: 83 additions & 0 deletions test/app.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
const app = require('../src/app');
const axios = require('axios');
const sinon = require('sinon');
const test = require('ava');
const uuid = require('uuid/v4');

const { stubTriggerBuild } = require('./stubs');

test.beforeEach((test) => {
const client = axios.create();
const slug = uuid();

test.context.app = app({ client, slug });
test.context.client = client;
test.context.slug = slug;
});

test('an app has a slug', (test) => {
const { app, slug } = test.context;
test.is(app.slug, slug);
});

test('an app can trigger a new build', async (test) => {
const { app, client, slug } = test.context;
const stub = stubTriggerBuild({ appSlug: slug, axios: client });

const build = await app.triggerBuild();
test.is(build.appSlug, slug);
test.is(build.buildSlug, stub.build.build_slug);

sinon.assert.calledOnce(client.post);
sinon.assert.calledWithExactly(
client.post,
sinon.match.string,
sinon.match({
build_params: { branch: 'master' },
hook_info: { type: 'bitrise' },
triggered_by: '@lifeomic/bitrise'
})
);
});

test('an app can trigger a build for a specific commit', async (test) => {
const { app, client, slug } = test.context;
const commitHash = uuid();
const stub = stubTriggerBuild({ appSlug: slug, axios: client });

const build = await app.triggerBuild({ commitHash });
test.is(build.appSlug, slug);
test.is(build.buildSlug, stub.build.build_slug);

sinon.assert.calledOnce(client.post);
sinon.assert.calledWithExactly(
client.post,
sinon.match.string,
sinon.match({
build_params: { commit_hash: commitHash },
hook_info: { type: 'bitrise' },
triggered_by: '@lifeomic/bitrise'
})
);
});

test('an app can trigger a specific build workflow', async (test) => {
const { app, client, slug } = test.context;
const stub = stubTriggerBuild({ appSlug: slug, axios: client });
const workflow = 'test';

const build = await app.triggerBuild({ workflow });
test.is(build.appSlug, slug);
test.is(build.buildSlug, stub.build.build_slug);

sinon.assert.calledOnce(client.post);
sinon.assert.calledWithExactly(
client.post,
sinon.match.string,
sinon.match({
build_params: { branch: 'master', workflow_id: workflow },
hook_info: { type: 'bitrise' },
triggered_by: '@lifeomic/bitrise'
})
);
});
Loading

0 comments on commit 0b66fa1

Please sign in to comment.