UNPKG

@qooxdoo/framework

Version:

The JS Framework for Coders

576 lines (520 loc) 17.7 kB
/* ************************************************************************ qooxdoo - the new era of web development http://qooxdoo.org Copyright: 2017 Christian Boulanger License: MIT: https://opensource.org/licenses/MIT See the LICENSE file in the project's top-level directory for details. Authors: * Christian Boulanger (info@bibliograph.org, @cboulanger) ************************************************************************ */ const fs = require("fs"); const path = require("upath"); const process = require("process"); const { Octokit } = require("@octokit/rest"); const semver = require("semver"); const inquirer = require("inquirer"); const glob = require("glob"); /** * Publishes a release on GitHub */ qx.Class.define("qx.tool.cli.commands.package.Publish", { extend: qx.tool.cli.commands.Package, statics: { getYargsCommand() { return { command: "publish", describe: "publishes a new release of the package on GitHub. Requires a GitHub access token. By default, makes a patch release.", builder: { type: { alias: "t", describe: "Set the release type", nargs: 1, choices: "major,premajor,minor,preminor,patch,prepatch,prerelease".split( /,/ ), type: "string" }, noninteractive: { alias: "I", type: "boolean", describe: "Do not prompt user" }, "use-version": { alias: "V", type: "string", describe: "Use given version number" }, prerelease: { type: "boolean", alias: "p", describe: "Publish as a prerelease (as opposed to a stable release)" }, quiet: { type: "boolean", alias: "q", describe: "No output" }, message: { alias: "m", type: "string", describe: "Set commit/release message" }, dryrun: { type: "boolean", describe: "Deprecated. Use --dry-run" }, "dry-run": { type: "boolean", alias: "d", describe: "Show result only, do not publish to GitHub" }, force: { type: "boolean", alias: "f", describe: "Ignore warnings (such as demo check)" }, "create-index": { type: "boolean", alias: "i", describe: "Create an index file (qooxdoo.json) with paths to Manifest.json files" }, "qx-version": { type: "string", check: argv => semver.valid(argv.qxVersion), describe: "A semver string. If given, the qooxdoo version for which to publish the package" }, breaking: { type: "boolean", describe: "Do not create a backwards-compatible release, i.e. allow compatibility with current version only" }, "qx-version-range": { type: "string", describe: "A semver range. If given, it overrides --qx-version and --breaking and sets this specific version range" } } }; } }, events: { /** * Fired before commit happens. Data is an object with * version: new_version, * argv: this.argv */ beforeCommit: "qx.event.type.Data" }, members: { /** * Publishes a new release of the package 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-package" to the list of GitHub topics. * */ async process() { await super.process(); // init const argv = this.argv; if (argv.dryrun) { qx.tool.compiler.Console.info( 'The "--dryrun" option is deprecated. Please use "--dry-run" instead.' ); argv.dryRun = true } // qooxdoo version let qxVersion = await this.getQxVersion(); if (fs.existsSync("Manifest.json")) { qxVersion = await this.getAppQxVersion(); } if (argv.verbose) { this.info(`Using qooxdoo version: ${qxVersion}`); } // check git status let status; try { status = await qx.tool.utils.Utils.exec("git status --porcelain"); } catch (e) { throw new qx.tool.utils.Utils.UserError( "Cannot determine remote repository." ); } this.debug(status); if (status.trim() !== "") { throw new qx.tool.utils.Utils.UserError( "Please commit or stash all remaining changes first." ); } status = await qx.tool.utils.Utils.exec( "git status --porcelain --branch" ); this.debug(status); if (status.includes("ahead")) { throw new qx.tool.utils.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) { qx.tool.compiler.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.utils.Utils.UserError( `GitHub access token required.` ); } const octokit = new Octokit({ auth: token }); // create index file first? if (argv.i) { await this.__createIndexFile(argv); } let libraries; let version; let manifestModels = []; let mainManifestModel; const cwd = process.cwd(); const registryModel = qx.tool.config.Registry.getInstance(); if (await registryModel.exists()) { // we have a qooxdoo.json index file containing the paths of libraries in the repository await registryModel.load(); libraries = registryModel.getValue("libraries"); for (let library of libraries) { let manifestModel = await new qx.tool.config.Abstract( qx.tool.config.Manifest.config ) .set({ baseDir: path.join(cwd, library.path) }) .load(); manifestModels.push(manifestModel); // use the first manifest or the one with a truthy property "main" as reference if (!version || library.main) { version = manifestModel.getValue("info.version"); mainManifestModel = manifestModel; } } } else { // read Manifest.json mainManifestModel = await qx.tool.config.Manifest.getInstance().load(); manifestModels.push(mainManifestModel); // prevent accidental publication of demo manifest. if ( !argv.force && mainManifestModel.getValue("provides.namespace").includes(".demo") ) { throw new qx.tool.utils.Utils.UserError( "This seems to be the library demo. Please go into the library root directory to publish the library." ); } libraries = [{ path: "." }]; } // version let old_version = mainManifestModel.getValue("info.version"); let new_version; if (argv.useVersion) { // use user-supplied value new_version = semver.coerce(argv.useVersion); if (!new_version) { throw new qx.tool.utils.Utils.UserError( `${argv.useVersion} is not a valid version number.` ); } new_version = new_version.toString(); } else { // use version number from manifest and increment it if (!semver.valid(old_version)) { throw new qx.tool.utils.Utils.UserError( "Invalid version number in Manifest. Must be a valid semver version (x.y.z)." ); } if (!argv.type) { argv.type = semver.prerelease(old_version) ? "prerelease" : "patch"; } argv.prerelease = Boolean(argv.prerelease) || argv.type === "prerelease" || argv.type === "prepatch" || argv.type === "preminor" || argv.type === "premajor"; new_version = semver.inc(old_version, argv.type); } // tag and repo name let tag = `v${new_version}`; let url; try { url = ( await qx.tool.utils.Utils.exec("git config --get remote.origin.url") ).trim(); } catch (e) { throw new qx.tool.utils.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) { this.debug(`>>> Repository: ${repo_name}`); } let repoExists = false; try { await octokit.repos.getReleaseByTag({ owner, repo, tag }); repoExists = true; } catch (e) {} if (repoExists) { throw new qx.tool.utils.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.getAllTopics({ owner, repo }); topics = result.data.names; } catch (e) { if (e.message.includes("Bad credentials")) { throw new qx.tool.utils.Utils.UserError(`Your token is invalid.`); } throw e; } // semver range of framework dependency let semver_range = this.argv.qxVersionRange; // use CLI-supplied range if (!semver_range) { // no CLI value if (this.argv.breaking) { // use current version only -> breaking semver_range = "^" + qxVersion; } else { // get current semver range -> backward-compatible semver_range = mainManifestModel.getValue( "requires.@qooxdoo/framework" ); if (!semver.satisfies(qxVersion, semver_range, { loose: true })) { // make it compatible with current version semver_range = `^${qxVersion} || ${semver_range}`; } } } // 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 from ${old_version} to ${new_version}, having a dependency on qooxdoo ${semver_range}, 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 manifestModel of manifestModels) { manifestModel .setValue("requires.@qooxdoo/framework", semver_range) .setValue("info.version", new_version); if (argv.dryRun) { if (!argv.quiet) { qx.tool.compiler.Console.info( `Dry run: Not committing ${manifestModel.getRelativeDataPath()} with the following content:` ); qx.tool.compiler.Console.info( JSON.stringify(manifestModel.getData(), null, 2) ); } } else { manifestModel.save(); } } // package.json, only supported in the root const package_json_path = path.join(process.cwd(), "package.json"); if (await fs.existsAsync(package_json_path)) { let data = await qx.tool.utils.Json.loadJsonAsync(package_json_path); data.version = new_version; if (this.argv.dryRun) { qx.tool.compiler.Console.info( "Dry run: Not changing package.json version..." ); } else { await qx.tool.utils.Json.saveJsonAsync(package_json_path, data); if (!this.argv.quiet) { qx.tool.compiler.Console.info(`Updated version in package.json.`); } } } await this.fireDataEventAsync("beforeCommit", { version: new_version, argv: this.argv }); if (argv.dryRun) { qx.tool.compiler.Console.info( `Dry run: not creating tag and release '${tag}' of ${repo_name}...` ); return; } // 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) { qx.tool.compiler.Console.info( `Creating tag and release '${tag}' of ${repo_name}...` ); } // commit and push const run = qx.tool.utils.Utils.run; try { await run("git", ["add", "--all"]); await run("git", ["commit", `-m "${message}"`, "--allow-empty"]); await run("git", ["push"]); let release_data = { owner, repo, tag_name: tag, target_commitish: "master", name: tag, body: message, draft: false, prerelease: argv.prerelease }; await octokit.repos.createRelease(release_data); if (!argv.quiet) { qx.tool.compiler.Console.info(`Published new version '${tag}'.`); } } catch (e) { throw new qx.tool.utils.Utils.UserError(e.message); } // add GitHub topic const topic = "qooxdoo-package"; if (!topics.includes(topic)) { topics.push(topic); await octokit.repos.replaceAllTopics({ owner, repo, names: topics }); if (!argv.quiet) { qx.tool.compiler.Console.info(`Added GitHub topic '${topic}'.`); } } run("git", ["pull"]); }, /** * Creates a qooxdoo.json file with paths to Manifest.json files in this repository * @private */ __createIndexFile: async argv => new Promise((resolve, reject) => { if (argv.verbose && !argv.quiet) { qx.tool.compiler.Console.info("Creating index file..."); } glob( qx.tool.config.Manifest.config.fileName, { matchBase: true }, async (err, files) => { if (err) { reject(err); } if (!files || !files.length) { reject( new qx.tool.utils.Utils.UserError( "No Manifest.json files could be found" ) ); } let mainpath; if (files.length > 1) { let choices = files.map(p => { let m = qx.tool.utils.Json.parseJson( fs.readFileSync(path.join(process.cwd(), p), "utf-8") ); return { name: m.info.name + (m.info.summary ? ": " + m.info.summary : ""), value: p }; }); let answer = await inquirer.prompt({ name: "mainpath", message: "Please choose the main library", type: "list", choices }); mainpath = answer.mainpath; } let data = { libraries: files.map(p => files.length > 1 && p === mainpath ? { path: path.dirname(p), main: true } : { path: path.dirname(p) } ) }; // write index file const registryModel = qx.tool.config.Registry.getInstance(); if (argv.dryRun) { qx.tool.compiler.Console.info( `Dry run: not creating index file ${registryModel.getRelativeDataPath()} with the following content:` ); qx.tool.compiler.Console.info(data); } else { await registryModel.load(data); await registryModel.save(); if (!argv.quiet) { qx.tool.compiler.Console.info( `Created index file ${registryModel.getRelativeDataPath()}'.` ); } } resolve(); } ); }) } });