diff --git a/.github/workflows/create-package-release.yml b/.github/workflows/create-package-release.yml new file mode 100644 index 0000000..4000126 --- /dev/null +++ b/.github/workflows/create-package-release.yml @@ -0,0 +1,67 @@ +name: Create Package Release + +on: + workflow_dispatch: + inputs: + versionType: + type: choice + description: 'Release Type' + options: + - minor + - major + required: true + +jobs: + create-release: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + with: + ref: main + + - name: Setup Git + run: | + git config --global user.name 'GitHub Actions' + git config --global user.email 'actions@github.com' + + ## Update the package json to either a major or minor version depending on the selection + + - name: Upgrade yarn with major version + if: ${{ github.event.inputs.versionType == 'major' }} + run: | + yarn version --major --no-git-tag-version + git add package.json + + - name: Upgrade yarn with minor version + if: ${{ github.event.inputs.versionType == 'minor' }} + run: | + yarn version --minor --no-git-tag-version + git add package.json + + # Create release branch off of main with the new version number in the branch name + - name: Create Release Branch + run: | + NEW_VERSION=$(npm pkg get version | tr -d '"') # Trim quotes wrapping command output + TIMESTAMP=$(date +'%Y%m%d%H%M%S') + BRANCH_NAME="release-changes-${NEW_VERSION}-${TIMESTAMP}" + git checkout -b $BRANCH_NAME + echo "BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_ENV + echo "NEW_VERSION=$NEW_VERSION" >> $GITHUB_ENV + + # Commit the changes of the package.json version increment. Format the commit message to be Release x.y.z so it's picked up by the publish action. + - name: Commit Release Changes + run: | + git commit -m "Release ${{ env.NEW_VERSION }}" + git push origin HEAD + + # Create a pull request so it can be merged back into main. + - name: Create Pull Request + uses: repo-sync/pull-request@v2 + with: + source_branch: ${{ env.BRANCH_NAME }} + destination_branch: "main" + github_token: ${{ secrets.GITHUB_TOKEN }} + pr_title: "Release CLI: Version ${{ env.NEW_VERSION }}" + pr_body: "Automated pull request to release the latest version of the CLI." + pr_label: "automated-pr" diff --git a/index.js b/index.js index 16cd625..86eb6cf 100755 --- a/index.js +++ b/index.js @@ -28,13 +28,7 @@ import { info } from "./scripts/info.js"; import { removeModules } from "./scripts/remove.js"; import { commitModules } from "./scripts/commit-module.js"; import { upgradeScaffold } from "./scripts/upgrade.js"; -import { - valid, - invalid, - isNameValid, - section, - isUserEnvironment -} from "./utils.js"; +import { valid, invalid, section, isUserEnvironment } from "./utils.js"; import { createModule } from "./scripts/create.js"; import { login } from "./scripts/login.js"; import { configFile } from "./scripts/utils/configFile.js"; @@ -51,6 +45,7 @@ import { HAS_ASKED_OPT_IN_NAME } from "./scripts/analytics/config.js"; import { EVENT } from "./scripts/analytics/constants.js"; import { askOptIn } from "./scripts/analytics/scripts.js"; import { sentryMonitoring } from "./scripts/utils/sentry.js"; +import { setModuleDetails } from "./scripts/setModuleDetails.js"; const pkg = JSON.parse( fs.readFileSync(new URL("package.json", import.meta.url), "utf8") @@ -182,27 +177,24 @@ const commands = { const args = arg({ "--name": String, "--type": String, - "--target": String + "--target": String, + "--search-description": String, + "--acceptance-criteria": String }); - if (!args["--name"]) { - invalid("missing required argument: --name"); - } - if (!args["--type"]) { - invalid("missing required argument: --type"); - } - if (!isNameValid(args["--name"])) { - invalid( - `invalid module name provided: '${args["--name"]}'. Use only alphanumeric characters, dashes and underscores.` - ); - } - analytics.sendEvent({ name: EVENT.CREATE_MODULE, properties: { Name: args["--name"] } }); - createModule(args["--name"], args["--type"], args["--target"], gitRoot()); + createModule( + args["--name"], + args["--type"], + args["--target"], + args["--search-description"], + args["--acceptance-criteria"], + gitRoot() + ); }, commit: () => { const args = arg({ @@ -312,7 +304,11 @@ demo`; "--visibility": String, "--status": String, "--page": String, - "--unarchive": Boolean + "--unarchive": Boolean, + "--name": String, + "--description": String, + "--acceptance-criteria": String, + "--search-description": String }); let id; @@ -347,6 +343,24 @@ demo`; await modulesGet(id); break; + case "set": + id = args._[2]; + + if (!id) { + return invalid( + "Please provide the id of the module to change info for, i.e. modules set <123>" + ); + } + + await setModuleDetails(id, + args["--name"], + args["--description"], + args["--acceptance-criteria"], + args["--search-description"] + ); + + break; + case "archive": id = args._[2]; if (!id) { @@ -418,6 +432,9 @@ Commands available: demo Generate a local React Native and Django demo app add Install a module in the demo app remove Remove a module from the demo app + get Get information about a module by id + set Set information about a module by id such as name, description, acceptance criteria, and search description. The new values must be wrapped in quotes "". + create Create a new module of a given type create Create a new module of a given type commit Update an existing module from the demo source code init Initialize a blank modules repository @@ -456,6 +473,13 @@ Install one or modules to your demo app: Remove one or modules from your demo app: cb remove +Get information about a module by id: + cb modules get + +Set information about a module by id such as name, description, acceptance criteria, and search description: + cb modules set --name "" --description "" --acceptance-criteria "" --search-description "" + The new values must be wrapped in quotes "". + Install modules from other directory: cb add --source ../other-repository diff --git a/scripts/create.js b/scripts/create.js index 8fb5742..1269461 100644 --- a/scripts/create.js +++ b/scripts/create.js @@ -10,6 +10,7 @@ import { generateMeta } from "./utils/templates.js"; import { execOptions, configurePython } from "./utils/environment.js"; +import inquirer from "inquirer"; function generateRNFiles(base, name, relative = "/") { if (relative !== "/") { @@ -45,7 +46,10 @@ function generateDjangoFiles(base, name, relative = "/") { ); const appsFileData = fs.readFileSync(`${innerAppPath}/apps.py`, "utf8"); - const result = appsFileData.replace(/name = '.*'/, `name = 'modules.django_${sanitizedName}.${sanitizedName}'`); + const result = appsFileData.replace( + /name = '.*'/, + `name = 'modules.django_${sanitizedName}.${sanitizedName}'` + ); fs.writeFileSync(`${innerAppPath}/apps.py`, result, "utf8"); fs.writeFileSync( @@ -60,7 +64,102 @@ function generateDjangoFiles(base, name, relative = "/") { ); } -export function createModule(name, type, target, gitRoot) { +const isNameValid = (name) => { + const pattern = /^[a-zA-Z][a-zA-Z0-9_-]*$/; + return pattern.test(name); +}; + +const getValidModuleInputs = async ( + initialName, + initialType, + initialSearchDescription, + initialAcceptanceCriteria +) => { + let name, type, searchDescription, acceptanceCriteria; + + if (initialName) { + name = initialName; + } else { + const { inputName } = await inquirer.prompt({ + message: "Module Name:", + name: "inputName", + type: "input" + }); + + name = inputName; + } + + if (initialType) { + type = initialType; + } else { + const { inputType } = await inquirer.prompt({ + message: "Module Type:", + name: "inputType", + type: "list", + choices: ["all", "react-native", "django"] + }); + + type = inputType; + } + + if (!name) { + invalid("missing required argument: --name"); + } + if (!type) { + invalid("missing required argument: --type"); + } + if (!isNameValid(name)) { + invalid( + `invalid module name provided: '${name}'. Use only alphanumeric characters, dashes and underscores.` + ); + } + + if (!initialName) { + section( + "The following fields help Crowdbotics match this module to application features. Please enter the following values (these can be updated later in the meta.json file, or in the Crowdbotics platform):" + ); + const { inputSearchDescription, inputAcceptanceCriteria } = + await inquirer.prompt([ + { + message: "Search Description:", + name: "inputSearchDescription", + type: "input", + default: initialSearchDescription || undefined + }, + { + message: "Acceptance Criteria:", + name: "inputAcceptanceCriteria", + type: "input", + default: initialAcceptanceCriteria || undefined + } + ]); + + searchDescription = inputSearchDescription; + acceptanceCriteria = inputAcceptanceCriteria; + } else { + searchDescription = initialSearchDescription; + acceptanceCriteria = initialAcceptanceCriteria; + } + + return { name, type, searchDescription, acceptanceCriteria }; +}; + +export async function createModule( + initialName, + initialType, + target, + initialSearchDescription, + initialAcceptanceCriteria, + gitRoot +) { + const { name, type, searchDescription, acceptanceCriteria } = + await getValidModuleInputs( + initialName, + initialType, + initialSearchDescription, + initialAcceptanceCriteria + ); + const cwd = process.cwd(); if (target) { @@ -83,7 +182,7 @@ export function createModule(name, type, target, gitRoot) { const dir = path.join(target, slug); if (existsSync(dir)) invalid(`module named "${slug}" already exists`); - const meta = generateMeta(name, type); + const meta = generateMeta(name, type, searchDescription, acceptanceCriteria); try { fs.mkdirSync(dir, { recursive: true }); diff --git a/scripts/setModuleDetails.js b/scripts/setModuleDetails.js new file mode 100644 index 0000000..0355242 --- /dev/null +++ b/scripts/setModuleDetails.js @@ -0,0 +1,42 @@ +import ora from "ora"; +import { invalid, valid } from "../utils.js"; +import { apiClient } from "./utils/apiClient.js"; + +export const setModuleDetails = async ( + id, name, description, searchDescription, acceptanceCriteria +) => { + const patchBody = {}; + + if (name) { + patchBody.title = name; + } + if (description) { + patchBody.description = description; + } + if (searchDescription) { + patchBody.search_description = searchDescription; + } + if (acceptanceCriteria) { + patchBody.acceptance_criteria = acceptanceCriteria; + } + if (Object.keys(patchBody).length === 0) { + invalid("No module details were provided for the update. To correctly save the new value, please enclose it in double quotes. For example, use --description \"Your detailed description here\"."); + return; + } + const patchSpinner = ora( + "Updating module details." + ).start(); + + const patchResponse = await apiClient.patch({ + path: `/v1/catalog/module/${id}`, + body: patchBody + }).then(patchSpinner.stop()); + + if (patchResponse.ok) { + valid(`Module details updated for ${id}.`); + } else if (patchResponse.status === 404) { + invalid(`Cannot find requested module with id ${id}.`); + } else { + invalid("Unable to update modules details. Please try again later."); + } +}; diff --git a/scripts/utils/templates.js b/scripts/utils/templates.js index ac1f0aa..7dd604e 100644 --- a/scripts/utils/templates.js +++ b/scripts/utils/templates.js @@ -68,7 +68,12 @@ export const packageJson = (name) => `{ /** * Miscellaneous */ -export function generateMeta(name, type) { +export function generateMeta( + name, + type, + searchDescription, + acceptanceCriteria +) { const rootMap = { all: "/", "react-native": `/modules/${name}`, @@ -78,6 +83,8 @@ export function generateMeta(name, type) { const meta = { title: name, description: "", + search_description: searchDescription || "", + acceptance_criteria: acceptanceCriteria || "", root: rootMap[type], schema: {} }; diff --git a/utils.js b/utils.js index 113f968..6730d3f 100644 --- a/utils.js +++ b/utils.js @@ -22,9 +22,4 @@ export const section = (...args) => { console.log(">", ...args); }; -export const isNameValid = (name) => { - const pattern = /^[a-zA-Z][a-zA-Z0-9_-]*$/; - return pattern.test(name); -}; - export const isUserEnvironment = !process?.env?.CI && !process?.env?.CIRCLE_JOB;