release-schemes/rtk.js

"use strict";
/**
 * @module release-schemes/rtk
 */
import async from "async";
import bag from "bagofcli";
import stringFormat from "string-format";
import util from "util";
import semverVersionScheme from "../version-schemes/semver.js";
import gitScmScheme from "../scm-schemes/git.js";
import hclResourceType from "../resource-types/hcl.js";
import jsonResourceType from "../resource-types/json.js";
import keepAChangelogResourceType from "../resource-types/keep-a-changelog.js";
import makefileResourceType from "../resource-types/makefile.js";
import textResourceType from "../resource-types/text.js";
import tomlResourceType from "../resource-types/toml.js";
import yamlResourceType from "../resource-types/yaml.js";

/**
 * Default release scheme for RTK, consisting of three steps:
 * pre (set and commit release version), release (tag release version),
 * and post (set and commit next pre-release version).
 */
class Rtk {
  /**
   * Construct release scheme with the version and SCM schemes to use.
   *
   * @param {String} [versionSchemeName=semver] - version scheme name
   * @param {String} [scmSchemeName=git] - SCM scheme name
   * @param {String} preReleaseVersion - current pre-release version
   * @param {Object} opts - release settings
   * @param {Boolean} [opts.dryRun] - when true, no change will be made
   * @param {String} [opts.releaseIncrementType] - major, minor, or patch
   * @param {String} [opts.postReleaseIncrementType] - major, minor, or patch
   */
  constructor(versionSchemeName, scmSchemeName, preReleaseVersion, opts) {
    versionSchemeName = versionSchemeName || "semver";
    scmSchemeName = scmSchemeName || "git";

    this.versionSchemes = {
      semver: semverVersionScheme,
    };
    this.scmSchemes = {
      git: gitScmScheme,
    };
    this.resourceTypes = {
      hcl: hclResourceType,
      json: jsonResourceType,
      makefile: makefileResourceType,
      text: textResourceType,
      toml: tomlResourceType,
      yaml: yamlResourceType,
    };
    this.resourceTypes["keep-a-changelog"] = keepAChangelogResourceType;

    const VersionSchemeClass = this.versionSchemes[versionSchemeName];
    this.versionScheme = new VersionSchemeClass(
      preReleaseVersion,
      opts.releaseIncrementType,
      opts.postReleaseIncrementType,
    );
    this.scmScheme = this.scmSchemes[scmSchemeName];

    this.logOpts = {};
    if (opts.dryRun) {
      this.logOpts["labels"] = ["dry run"];
    }
  }

  /**
   * Pre step: set release version on all resources, then commit the changes.
   * A resource's params.release_value overrides the release version, and
   * params.release_format formats it, e.g. v{version}.
   *
   * @param {Array<Object>} resources - resources which contain version value
   * @param {Object} opts - release settings
   * @param {Boolean} [opts.dryRun] - when true, no change will be made
   * @param {Function} cb - standard cb(err, result) callback
   */
  pre(resources, opts, cb) {
    const self = this;

    bag.logStepHeading(
      "Executing pre step of rtk release scheme...",
      self.logOpts,
    );

    const tasks = [];
    const paths = [];
    resources.forEach((resource) => {
      function resourceTask(cb) {
        let releaseVersion =
          resource.params && resource.params.release_value
            ? resource.params.release_value
            : self.versionScheme.getReleaseVersion();
        if (resource.params && resource.params.release_format) {
          releaseVersion = stringFormat(resource.params.release_format, {
            version: releaseVersion,
          });
        }
        const message = util.format(
          "Setting release version %s on %s resource located at %s",
          releaseVersion,
          resource.type,
          resource.path,
        );
        bag.logStepItemSuccess(message, self.logOpts);
        self.resourceTypes[resource.type].setReleaseVersion(
          releaseVersion,
          resource,
          opts,
          cb,
        );
      }
      tasks.push(resourceTask);
      paths.push(resource.path);
    });

    function scmTask(cb) {
      const message = util.format(
        "Committing release version changes made to %s...",
        paths.join(", "),
      );
      bag.logStepItemSuccess(message, self.logOpts);
      if (!opts.dryRun) {
        self.scmScheme.saveChanges(
          util.format(
            "Release version %s",
            self.versionScheme.getReleaseVersion(),
          ),
          paths,
          cb,
        );
      } else {
        cb();
      }
    }
    tasks.push(scmTask);

    async.series(tasks, cb);
  }

  /**
   * Release step: tag the release version.
   *
   * @param {Array<Object>} resources - resources which contain version value
   * @param {Object} opts - release settings
   * @param {Boolean} [opts.dryRun] - when true, no change will be made
   * @param {String} opts.tagFormat - SCM tag format, e.g. v{version}
   * @param {Function} cb - standard cb(err, result) callback
   */
  release(resources, opts, cb) {
    const self = this;

    bag.logStepHeading(
      "Executing release step of rtk release scheme...",
      self.logOpts,
    );

    const tasks = [];

    function scmTask(cb) {
      // Use tagFormat option to format the version to be used as SCM tag
      const formattedVersion = stringFormat(opts.tagFormat, {
        version: self.versionScheme.getReleaseVersion(),
      });
      const message = util.format(
        "Adding release version tag %s ...",
        formattedVersion,
      );
      bag.logStepItemSuccess(message, self.logOpts);
      if (!opts.dryRun) {
        self.scmScheme.addVersion(formattedVersion, cb);
      } else {
        cb();
      }
    }
    tasks.push(scmTask);

    async.series(tasks, cb);
  }

  /**
   * Post step: set next pre-release version on all resources, then commit
   * the changes. A resource's params.post_release_value overrides the
   * post-release version.
   *
   * @param {Array<Object>} resources - resources which contain version value
   * @param {Object} opts - release settings
   * @param {Boolean} [opts.dryRun] - when true, no change will be made
   * @param {Function} cb - standard cb(err, result) callback
   */
  post(resources, opts, cb) {
    const self = this;

    bag.logStepHeading(
      "Executing post step of rtk release scheme...",
      self.logOpts,
    );

    const tasks = [];
    const paths = [];
    resources.forEach((resource) => {
      function resourceTask(cb) {
        const postReleaseVersion =
          resource.params && resource.params.post_release_value
            ? resource.params.post_release_value
            : self.versionScheme.getPostReleaseVersion();
        const message = util.format(
          "Setting next pre-release version %s on %s resource located at %s",
          postReleaseVersion,
          resource.type,
          resource.path,
        );
        bag.logStepItemSuccess(message, self.logOpts);
        self.resourceTypes[resource.type].setPostReleaseVersion(
          postReleaseVersion,
          resource,
          opts,
          cb,
        );
      }
      tasks.push(resourceTask);
      paths.push(resource.path);
    });

    function scmTask(cb) {
      const message = util.format(
        "Committing next pre-release version changes made to %s...",
        paths.join(", "),
      );
      bag.logStepItemSuccess(message, self.logOpts);
      if (!opts.dryRun) {
        self.scmScheme.saveChanges(
          util.format(
            "Bump up to next pre-release version %s",
            self.versionScheme.getPostReleaseVersion(),
          ),
          paths,
          cb,
        );
      } else {
        cb();
      }
    }
    tasks.push(scmTask);

    async.series(tasks, cb);
  }
}

export { Rtk as default };