Source: index.js

/**
 * @copyright Copyright 2016 Kevin Locke <kevin@kevinlocke.name>
 * @license MIT
 */

'use strict';

const assert = require('assert');
const http = require('http');
const https = require('https');
const nodeify = require('promise-nodeify');

const GitStatusChecker = require('./lib/git-status-checker');
const TravisStatusChecker = require('./lib/travis-status-checker');

/** Checks that a build has an expected commit hash.
 * @param {!{commit:!{sha: string}}} build Build (or branch) object returned
 * by the Travis CI API.
 * @param {!{sha: string, name: ?string}} localCommit Expected commit
 * information.
 * @return {!Object} <code>build</code>
 * @throws AssertionError If <code>build.commit.sha</code> is not equal to
 * <code>expected</code>.
 */
function checkBuildCommit(build, localCommit) {
  const buildCommit = build.commit;
  let message = `Build commit ${buildCommit.sha
  } does not match ${localCommit.sha}`;
  if (localCommit.name) {
    message += ` (${localCommit.name})`;
  }
  // assert gives us useful exception properties for callers
  assert.strictEqual(
    buildCommit.sha,
    localCommit.sha,
    message,
  );
  return build;
}

/** Options for {@link travisStatus}.
 *
 * @typedef {{
 *   apiEndpoint: string|undefined,
 *   branch: string|boolean|undefined,
 *   commit: string|undefined,
 *   err: stream.Writable|undefined,
 *   in: stream.Readable|undefined,
 *   out: stream.Writable|undefined,
 *   repo: string|undefined,
 *   requestOpts: Object|undefined,
 *   storeRepo: string|undefined,
 *   token: string|undefined,
 *   wait: number|undefined
 * }} TravisStatusOptions
 * @property {boolean=} interactive behave as if being run interactively
 * @property {string=} apiEndpoint Travis API server to talk to
 * @property {(string|boolean)=} branch query latest build for named branch,
 * or the current branch
 * @property {string=} commit require build to be for a specific commit
 * @property {stream.Writable=} err Stream to which errors (and non-output
 * status messages) are written. (default: <code>process.stderr</code>)
 * @property {stream.Readable=} in Stream from which input is read. (default:
 * <code>process.stdin</code>)
 * @property {stream.Writable=} out Stream to which output is written.
 * (default: <code>process.stdout</code>)
 * @property {string=} repo repository to use (default: will try to detect from
 * current git clone)
 * @property {Object=} requestOpts Options for Travis CI API requests (suitable
 * for the {@link https://www.npmjs.com/package/request request module}).
 * Callers are encouraged to pass the <code>agent</code> or
 * <code>forever</code> options to leverage TCP keep-alive across requests.
 * @property {string=} storeRepo repository value (as described for
 * <code>repo</code>) to store permanently for future use.  Is used for this
 * invocation if <code>repo</code> is not set.
 * @property {string=} token access token to use
 * @property {number=} wait wait if build is pending (timeout in milliseconds)
 */
// var TravisStatusOptions;

/** Gets the current Travis CI status of a repo/branch.
 *
 * @param {?TravisStatusOptions=} options Options.
 * @param {?function(Error, Object=)=} callback Callback function called
 * with the current build information from the Travis CI API, or an
 * <code>Error</code> if it could not be retrieved.
 * @return {!Promise<!Object>|undefined} If <code>callback</code> is not given,
 * a <code>Promise</code> with the current build information from the Travis CI
 * API, or <code>Error</code> if it could not be retrieved.
 * Otherwise <code>undefined</code>.
 */
function travisStatus(options, callback) {
  if (!callback && typeof options === 'function') {
    callback = options;
    options = null;
  }

  if (callback && typeof callback !== 'function') {
    throw new TypeError('callback must be a function');
  }

  let agent, gitChecker, travisChecker;
  try {
    if (options && typeof options !== 'object') {
      throw new TypeError('options must be an object');
    }
    options = options || {};

    if (options.repo) {
      GitStatusChecker.checkSlugFormat(options.repo);
    }
    if (options.storeRepo) {
      GitStatusChecker.checkSlugFormat(options.storeRepo);
    }

    // If the caller didn't request an agent behavior, control it ourselves.
    // Each function call will use HTTP keep-alive for the duration of the
    // function, but not after completion, which callers may not expect.
    let { requestOpts } = options;
    if (!requestOpts
        || (requestOpts.agent === undefined
         && requestOpts.agentClass === undefined
         && requestOpts.agentOptions === undefined
         && requestOpts.forever === undefined
         && requestOpts.pool === undefined)) {
      const apiUrl =
        new URL(options.apiEndpoint || TravisStatusChecker.ORG_URI);
      const Agent = apiUrl.protocol === 'https:' ? https.Agent
        : apiUrl.protocol === 'http:' ? http.Agent
          : null;
      if (Agent) {
        agent = new Agent({ keepAlive: true });
        // .destroy() and keepAlive added to Agent in 0.11.4, nodejs@9fc9b874
        // If Agent doesn't support keepAlive/destroy, we don't need/want it.
        if (typeof agent.destroy === 'function') {
          requestOpts = { ...requestOpts };
          requestOpts.agent = agent;
          options = { ...options };
          options.requestOpts = requestOpts;
        } else {
          agent = undefined;
        }
      }
    }

    gitChecker = new GitStatusChecker(options);
    travisChecker = new TravisStatusChecker(options);
  } catch (errOptions) {
    const errResult = Promise.reject(errOptions);
    return nodeify(errResult, callback);
  }

  let repoSlugP;
  if (options.storeRepo) {
    const storedSlugP = gitChecker.tryStoreSlug(options.storeRepo);
    // If both .repo and .storeRepo are present, store .storeRepo and use .repo
    repoSlugP =
      options.repo ? storedSlugP.then(() => options.repo)
        : storedSlugP;
  } else if (options.repo) {
    repoSlugP = Promise.resolve(options.repo);
  } else {
    const foundSlugP = gitChecker.findSlug()
      .then(GitStatusChecker.checkSlugFormat);
    if (options.interactive) {
      repoSlugP = foundSlugP.then((slug) => gitChecker.tryStoreSlug(slug));
    } else {
      repoSlugP = foundSlugP;
    }
  }

  let localCommitP;
  if (options.commit) {
    localCommitP = gitChecker.resolveHash(options.commit)
      .then((resolved) => {
        const localCommit = { sha: resolved };
        if (resolved !== options.commit) {
          localCommit.name = options.commit;
        }
        return localCommit;
      });
  }

  // Before doing remote queries, ensure that there are no errors locally
  const slugForQueryP = Promise.all([repoSlugP, localCommitP])
    .then((slugAndHash) => slugAndHash[0]);

  let resultP;
  if (options.branch) {
    const branchP = options.branch === true ? gitChecker.detectBranch()
      : Promise.resolve(options.branch);
    resultP = Promise.all([slugForQueryP, branchP])
      .then((results) => {
        const slug = results[0];
        const branch = results[1];
        return travisChecker.getBranch(slug, branch, options);
      });
  } else {
    const repoP =
      slugForQueryP.then((slug) => travisChecker.getRepo(slug, options));

    if (localCommitP) {
      // Add build information to result
      resultP = repoP.then((repo) => travisChecker.getBuild(
        repo.repo.slug,
        repo.repo.last_build_id,
      )
        .then((build) => ({ ...repo, ...build })));
    } else {
      resultP = repoP;
    }
  }

  let checkedResultP = resultP;
  if (localCommitP) {
    checkedResultP = Promise.all([resultP, localCommitP])
      .then((all) => {
        const result = all[0];
        const localCommit = all[1];
        checkBuildCommit(result, localCommit);
        return result;
      });
  }

  let cleanupP;
  if (agent) {
    cleanupP = checkedResultP.then(
      (result) => { agent.destroy(); return result; },
      (err) => { agent.destroy(); return Promise.reject(err); },
    );
  } else {
    cleanupP = checkedResultP;
  }

  return nodeify(cleanupP, callback);
}

module.exports = travisStatus;
module.exports.ORG_URI = TravisStatusChecker.ORG_URI;
module.exports.PRO_URI = TravisStatusChecker.PRO_URI;
module.exports.GitStatusChecker = GitStatusChecker;
module.exports.TravisStatusChecker = TravisStatusChecker;