lib/qx/tool/cli/commands/contrib/Publish.js
async function() {
      // init
      const argv = this.argv;

      // qooxdoo version
      let qooxdoo_version = await this.getUserQxVersion();
      if (argv.verbose) {
        console.log(`>>> qooxdoo version:  ${qooxdoo_version}`);
      }

      // check git status
      let status = await this.exec("git status --porcelain");
      if (status.trim() !=="") {
        throw new qx.tool.cli.Utils.UserError("Please commit or stash all remaining changes first.");
      }
      status = await this.exec("git status --porcelain --branch");
      if (status.includes("ahead")) {
        throw new qx.tool.cli.Utils.UserError("Please push all local commits to GitHub first.");
      }

      // token
      let cfg = await qx.tool.cli.ConfigDb.getInstance();
      let github = cfg.db("github", {});
      if (!github.token) {
        let response = await inquirer.prompt([
          {
            type: "input",
            name: "token",
            message: "Publishing to GitHub requires an API token - visit https://github.com/settings/tokens to obtain one " +
                "(you must assign permission to publish);\nWhat is your GitHub API Token ? "
          }
        ]
        );
        if (!response.token) {
          console.error("You have not provided a GitHub token.");
          return;
        }
        github.token = response.token;
        cfg.save();
      }
      let token = github.token;
      if (!token) {
        throw new qx.tool.cli.Utils.UserError(`GitHub access token required.`);
      }
      octokit.authenticate({
        type: "token",
        token
      });

      // create index file first?
      if (argv.i) {
        await this.__createIndexFile();
      }

      let libraries;
      let manifest_path;
      let manifest;
      let version;
      let index_path = process.cwd() + "/qooxdoo.json";
      if (fs.existsSync(index_path)) {
        // we have a qooxdoo.json index file containing the paths of libraries in the repository
        let index = qx.tool.compiler.utils.Json.parseJson(fs.readFileSync(index_path, "utf-8"));
        if (index.contribs) {
          console.warn("qooxdoo.json/contribs is deprecated, use qooxdoo.json/libraries instead");
        }
        libraries = index.libraries || index.contribs;
        for (let library of libraries) {
          manifest_path = path.join(process.cwd(), library.path, "Manifest.json");
          if (!fs.existsSync(manifest_path)) {
            throw new qx.tool.cli.Utils.UserError(`Invalid path in qooxdoo.json: ${library.path}/Manifest.json does not exist.`);
          }
          let m = qx.tool.compiler.utils.Json.parseJson(fs.readFileSync(manifest_path, "utf-8"));
          // use the first manifest or the one with a truthy property "main" as reference
          if (!version || library.main) {
            version = m.info.version;
            manifest = m;
          }
        }
      } else {
        // read Manifest.json
        manifest_path = path.join(process.cwd(), "Manifest.json");
        if (!fs.existsSync(manifest_path)) {
          throw new qx.tool.cli.Utils.UserError("Cannot find Manifest.json - are you in the project directory?");
        }
        manifest = qx.tool.compiler.utils.Json.parseJson(fs.readFileSync(manifest_path, "utf-8"));

        // prevent accidental publication of demo manifest.
        if (!argv.force && manifest.provides.namespace.includes(".demo")) {
          throw new qx.tool.cli.Utils.UserError("This seems to be the contrib demo. Please go into the library directory to publish the library.");
        }
        libraries = [{ path: "."}];
      }


      // version
      let new_version;
      if (argv.version) {
        // use user-supplied value
        new_version = argv.version;
        if (!semver.valid(new_version)) {
          throw new qx.tool.cli.Utils.UserError(`${new_version} is not a valid version number.`);
        }
      } else {
        // use version number from manifest and increment it
        let old_version = manifest.info.version;
        if (!semver.valid(old_version)) {
          throw new qx.tool.cli.Utils.UserError("Invalid version number in Manifest. Must be a valid semver version (x.y.z).");
        }
        new_version = semver.inc(old_version, argv.type);
      }

      // tag and repo name
      let tag = `v${new_version}`;
      let url;
      try {
        url = (await this.exec("git config --get remote.origin.url")).trim();
      } catch (e) {
        throw new qx.tool.cli.Utils.UserError("Cannot determine remote repository.");
      }
      let repo_name = url.replace(/(https:\/\/github.com\/|git@github.com:)/, "").replace(/\.git/, "");
      let [owner, repo] = repo_name.split(/\//);
      if (argv.verbose) {
        console.log(`>>> Repository:  ${repo_name}`);
      }
      let repoExists = false;
      try {
        await octokit.repos.getReleaseByTag({owner, repo, tag});
        repoExists = true;
      } catch (e) {}
      if (repoExists) {
        throw new qx.tool.cli.Utils.UserError(`A release with tag '${tag} already exists.'`);
      }

      // get topics, this will also check credentials
      let result;
      let topics;
      try {
        result = await octokit.repos.listTopics({owner,
          repo});
        topics = result.data.names;
      } catch (e) {
        if (e.message.includes("Bad credentials")) {
          throw new qx.tool.cli.Utils.UserError(`Your token is invalid.`);
        }
        throw e;
      }

      // prompt user to confirm
      let doRelease = true;
      if (!argv.noninteractive) {
        let question = {
          type: "confirm",
          name: "doRelease",
          message: `This will ${argv.version?"set":"increment"} the version to ${new_version} and create a release of the current master on GitHub. Do you want to proceed?`,
          default: "y"
        };
        let answer = await inquirer.prompt(question);
        doRelease = answer.doRelease;
      }
      if (!doRelease) {
        process.exit(0);
      }

      // update Manifest(s)
      for (let library of libraries) {
        manifest_path = path.join(process.cwd(), library.path, "Manifest.json");
        manifest = qx.tool.compiler.utils.Json.parseJson(fs.readFileSync(manifest_path, "utf-8"));

        manifest.info.version = new_version;

        // qooxdoo-versions @deprecated but still suppored
        let qooxdoo_versions = manifest.info["qooxdoo-versions"];
        if (qooxdoo_versions instanceof Array) {
          if (!qooxdoo_versions.includes(qooxdoo_version)) {
            manifest.info["qooxdoo-versions"].push(qooxdoo_version);
          }
        }

        // qooxdoo-range @deprecated
        let needWrite = false;
        let semver_range;
        if (manifest.info["qooxdoo-range"]) {
          if (!argv.quiet) {
            console.info(`Deprecated key "manifest.info.qooxdoo-range" found, will be changed to manifest.requires.qooxdoo-sdk`);
          }
          semver_range = manifest.info["qooxdoo-range"];
          delete manifest.info["qooxdoo-range"];
          needWrite = true;
        }
        semver_range = (manifest.requires && manifest.requires["qooxdoo-sdk"]) || semver_range;

        if (!semver.satisfies(qooxdoo_version, semver_range, {loose: true})) {
          needWrite = true;
          semver_range += (semver_range ? " || ^" : "^") + qooxdoo_version;
        }
        if (needWrite) {
          if (!manifest.requires) {
            manifest.requires = {};
          }
          manifest.requires["qooxdoo-sdk"] = semver_range;
        }

        let manifest_content = JSON.stringify(manifest, null, 2);
        if (argv.dryrun) {
          if (!argv.quiet) {
            console.info("Dry run: Not committing the following Manifest:");
            console.info(manifest_content);
          }
        } else {
          fs.writeFileSync(manifest_path, manifest_content, "utf-8");
        }
      }

      if (argv.dryrun) {
        if (!argv.quiet) {
          console.info(`Dry run: not creating tag and release '${tag}' of ${repo_name}...`);
        }
        process.exit(0);
      }

      // commit message
      let message;
      if (argv.message) {
        message = argv.message.replace(/"/g, "\\\"");
      } else if (!argv.noninteractive) {
        let question = {
          type: "input",
          name: "message",
          message: `Please enter a commit message:`
        };
        let answer = await inquirer.prompt([question]);
        message = answer.message;
      }
      if (!message) {
        message = `Release v${new_version}`;
      }

      if (!argv.quiet) {
        console.info(`Creating tag and release '${tag}' of ${repo_name}...`);
      }

      // commit and push
      try {
        await this.run("git", ["add", "--all"]);
        await this.run("git", ["commit", `-m "${message}"`]);
        await this.run("git", ["push"]);
        let release_data = {
          owner,
          repo,
          tag_name: tag,
          target_commitish: "master",
          name: tag,
          body: message,
          draft: false,
          prerelease: (argv.type.indexOf("pre")>=0)
        };
        await octokit.repos.createRelease(release_data);
        if (!argv.quiet) {
          console.info(`Published new version '${tag}'.`);
        }
      } catch (e) {
        throw new qx.tool.cli.Utils.UserError(e.message);
      }
      // add GitHub topic
      const topic = "qooxdoo-contrib";
      if (!topics.includes(topic)) {
        topics.push(topic);
        await octokit.repos.replaceTopics({owner,
          repo,
          names:topics});
        if (!argv.quiet) {
          console.info(`Added GitHub topic '${topic}'.`);
        }
      }
    }
async function() {
      // init
      const argv = this.argv;

      // qooxdoo version
      let qooxdoo_version = await this.getUserQxVersion();
      if (argv.verbose) {
        console.log(`>>> qooxdoo version:  ${qooxdoo_version}`);
      }

      // check git status
      let status = await this.exec("git status --porcelain");
      if (status.trim() !=="") {
        throw new qx.tool.cli.Utils.UserError("Please commit or stash all remaining changes first.");
      }
      status = await this.exec("git status --porcelain --branch");
      if (status.includes("ahead")) {
        throw new qx.tool.cli.Utils.UserError("Please push all local commits to GitHub first.");
      }

      // token
      let cfg = await qx.tool.cli.ConfigDb.getInstance();
      let github = cfg.db("github", {});
      if (!github.token) {
        let response = await inquirer.prompt([
          {
            type: "input",
            name: "token",
            message: "Publishing to GitHub requires an API token - visit https://github.com/settings/tokens to obtain one " +
                "(you must assign permission to publish);\nWhat is your GitHub API Token ? "
          }
        ]
        );
        if (!response.token) {
          console.error("You have not provided a GitHub token.");
          return;
        }
        github.token = response.token;
        cfg.save();
      }
      let token = github.token;
      if (!token) {
        throw new qx.tool.cli.Utils.UserError(`GitHub access token required.`);
      }
      octokit.authenticate({
        type: "token",
        token
      });

      // create index file first?
      if (argv.i) {
        await this.__createIndexFile();
      }

      let libraries;
      let manifest_path;
      let manifest;
      let version;
      let index_path = process.cwd() + "/qooxdoo.json";
      if (fs.existsSync(index_path)) {
        // we have a qooxdoo.json index file containing the paths of libraries in the repository
        let index = qx.tool.compiler.utils.Json.parseJson(fs.readFileSync(index_path, "utf-8"));
        if (index.contribs) {
          console.warn("qooxdoo.json/contribs is deprecated, use qooxdoo.json/libraries instead");
        }
        libraries = index.libraries || index.contribs;
        for (let library of libraries) {
          manifest_path = path.join(process.cwd(), library.path, "Manifest.json");
          if (!fs.existsSync(manifest_path)) {
            throw new qx.tool.cli.Utils.UserError(`Invalid path in qooxdoo.json: ${library.path}/Manifest.json does not exist.`);
          }
          let m = qx.tool.compiler.utils.Json.parseJson(fs.readFileSync(manifest_path, "utf-8"));
          // use the first manifest or the one with a truthy property "main" as reference
          if (!version || library.main) {
            version = m.info.version;
            manifest = m;
          }
        }
      } else {
        // read Manifest.json
        manifest_path = path.join(process.cwd(), "Manifest.json");
        if (!fs.existsSync(manifest_path)) {
          throw new qx.tool.cli.Utils.UserError("Cannot find Manifest.json - are you in the project directory?");
        }
        manifest = qx.tool.compiler.utils.Json.parseJson(fs.readFileSync(manifest_path, "utf-8"));

        // prevent accidental publication of demo manifest.
        if (!argv.force && manifest.provides.namespace.includes(".demo")) {
          throw new qx.tool.cli.Utils.UserError("This seems to be the contrib demo. Please go into the library directory to publish the library.");
        }
        libraries = [{ path: "."}];
      }


      // version
      let new_version;
      if (argv.version) {
        // use user-supplied value
        new_version = argv.version;
        if (!semver.valid(new_version)) {
          throw new qx.tool.cli.Utils.UserError(`${new_version} is not a valid version number.`);
        }
      } else {
        // use version number from manifest and increment it
        let old_version = manifest.info.version;
        if (!semver.valid(old_version)) {
          throw new qx.tool.cli.Utils.UserError("Invalid version number in Manifest. Must be a valid semver version (x.y.z).");
        }
        new_version = semver.inc(old_version, argv.type);
      }

      // tag and repo name
      let tag = `v${new_version}`;
      let url;
      try {
        url = (await this.exec("git config --get remote.origin.url")).trim();
      } catch (e) {
        throw new qx.tool.cli.Utils.UserError("Cannot determine remote repository.");
      }
      let repo_name = url.replace(/(https:\/\/github.com\/|git@github.com:)/, "").replace(/\.git/, "");
      let [owner, repo] = repo_name.split(/\//);
      if (argv.verbose) {
        console.log(`>>> Repository:  ${repo_name}`);
      }
      let repoExists = false;
      try {
        await octokit.repos.getReleaseByTag({owner, repo, tag});
        repoExists = true;
      } catch (e) {}
      if (repoExists) {
        throw new qx.tool.cli.Utils.UserError(`A release with tag '${tag} already exists.'`);
      }

      // get topics, this will also check credentials
      let result;
      let topics;
      try {
        result = await octokit.repos.listTopics({owner,
          repo});
        topics = result.data.names;
      } catch (e) {
        if (e.message.includes("Bad credentials")) {
          throw new qx.tool.cli.Utils.UserError(`Your token is invalid.`);
        }
        throw e;
      }

      // prompt user to confirm
      let doRelease = true;
      if (!argv.noninteractive) {
        let question = {
          type: "confirm",
          name: "doRelease",
          message: `This will ${argv.version?"set":"increment"} the version to ${new_version} and create a release of the current master on GitHub. Do you want to proceed?`,
          default: "y"
        };
        let answer = await inquirer.prompt(question);
        doRelease = answer.doRelease;
      }
      if (!doRelease) {
        process.exit(0);
      }

      // update Manifest(s)
      for (let library of libraries) {
        manifest_path = path.join(process.cwd(), library.path, "Manifest.json");
        manifest = qx.tool.compiler.utils.Json.parseJson(fs.readFileSync(manifest_path, "utf-8"));

        manifest.info.version = new_version;

        // qooxdoo-versions @deprecated but still suppored
        let qooxdoo_versions = manifest.info["qooxdoo-versions"];
        if (qooxdoo_versions instanceof Array) {
          if (!qooxdoo_versions.includes(qooxdoo_version)) {
            manifest.info["qooxdoo-versions"].push(qooxdoo_version);
          }
        }

        // qooxdoo-range @deprecated
        let needWrite = false;
        let semver_range;
        if (manifest.info["qooxdoo-range"]) {
          if (!argv.quiet) {
            console.info(`Deprecated key "manifest.info.qooxdoo-range" found, will be changed to manifest.requires.qooxdoo-sdk`);
          }
          semver_range = manifest.info["qooxdoo-range"];
          delete manifest.info["qooxdoo-range"];
          needWrite = true;
        }
        semver_range = (manifest.requires && manifest.requires["qooxdoo-sdk"]) || semver_range;

        if (!semver.satisfies(qooxdoo_version, semver_range, {loose: true})) {
          needWrite = true;
          semver_range += (semver_range ? " || ^" : "^") + qooxdoo_version;
        }
        if (needWrite) {
          if (!manifest.requires) {
            manifest.requires = {};
          }
          manifest.requires["qooxdoo-sdk"] = semver_range;
        }

        let manifest_content = JSON.stringify(manifest, null, 2);
        if (argv.dryrun) {
          if (!argv.quiet) {
            console.info("Dry run: Not committing the following Manifest:");
            console.info(manifest_content);
          }
        } else {
          fs.writeFileSync(manifest_path, manifest_content, "utf-8");
        }
      }

      if (argv.dryrun) {
        if (!argv.quiet) {
          console.info(`Dry run: not creating tag and release '${tag}' of ${repo_name}...`);
        }
        process.exit(0);
      }

      // commit message
      let message;
      if (argv.message) {
        message = argv.message.replace(/"/g, "\\\"");
      } else if (!argv.noninteractive) {
        let question = {
          type: "input",
          name: "message",
          message: `Please enter a commit message:`
        };
        let answer = await inquirer.prompt([question]);
        message = answer.message;
      }
      if (!message) {
        message = `Release v${new_version}`;
      }

      if (!argv.quiet) {
        console.info(`Creating tag and release '${tag}' of ${repo_name}...`);
      }

      // commit and push
      try {
        await this.run("git", ["add", "--all"]);
        await this.run("git", ["commit", `-m "${message}"`]);
        await this.run("git", ["push"]);
        let release_data = {
          owner,
          repo,
          tag_name: tag,
          target_commitish: "master",
          name: tag,
          body: message,
          draft: false,
          prerelease: (argv.type.indexOf("pre")>=0)
        };
        await octokit.repos.createRelease(release_data);
        if (!argv.quiet) {
          console.info(`Published new version '${tag}'.`);
        }
      } catch (e) {
        throw new qx.tool.cli.Utils.UserError(e.message);
      }
      // add GitHub topic
      const topic = "qooxdoo-contrib";
      if (!topics.includes(topic)) {
        topics.push(topic);
        await octokit.repos.replaceTopics({owner,
          repo,
          names:topics});
        if (!argv.quiet) {
          console.info(`Added GitHub topic '${topic}'.`);
        }
      }
    }
Publishes a new release of the contrib on GitHub, by executing the following steps:

1. In Manifest.json, update the qooxdoo-range value to include the version of the qooxdoo
framework (As per package.json).
2. In Manifest.json, based the given options, increment the version number (patch,
feature, breaking).
3. Create a release with the tag vX.Y.Z according to the current version.
4. Add "qooxdoo-contrib" to the list of GitHub topics.