From 0513100af04c75f3e0cd46973f742affb278f6da Mon Sep 17 00:00:00 2001 From: km1chno Date: Wed, 18 Dec 2024 17:07:23 +0100 Subject: [PATCH 1/3] feat: add simple multibranch --- __tests__/cli.test.ts | 19 +- __tests__/recipes/detox.test.ts | 14 +- __tests__/recipes/jest.test.ts | 8 +- __tests__/recipes/lint.test.ts | 14 +- __tests__/recipes/prettier.test.ts | 8 +- __tests__/recipes/typescript.test.ts | 8 +- __tests__/runtime-version-file.test.ts | 5 +- __tests__/utils.ts | 2 +- src/commands/help.ts | 17 +- src/commands/setup-ci.ts | 101 +++++----- src/constants.ts | 23 ++- src/extensions/config.ts | 187 +++++++++++++----- src/extensions/executor.ts | 45 +++++ src/extensions/interactive.ts | 50 +++-- src/extensions/options.ts | 55 +++++- src/extensions/projectConfig.ts | 52 ++++- src/extensions/workflows.ts | 24 ++- src/recipes/build.ts | 109 +++++----- src/recipes/detox.ts | 45 +++-- src/recipes/eas.ts | 27 ++- src/recipes/jest.ts | 31 ++- src/recipes/lint.ts | 33 +++- src/recipes/maestro.ts | 57 ++++-- src/recipes/prettier.ts | 33 +++- src/recipes/typescript.ts | 33 +++- .../build-release/build-release-android.ejf | 5 +- .../build-release/build-release-ios.ejf | 5 +- src/templates/common/triggerEvents.ejs | 13 ++ src/templates/detox/test-detox-android.ejf | 5 +- src/templates/detox/test-detox-ios.ejf | 5 +- src/templates/eas/eas.ejf | 5 +- src/templates/jest/jest.ejf | 5 +- src/templates/lint/lint.ejf | 7 +- .../maestro/maestro-test-android.ejf | 5 +- src/templates/maestro/maestro-test-ios.ejf | 5 +- src/templates/prettier/prettier.ejf | 5 +- src/templates/typescript/typescript.ejf | 5 +- src/types.ts | 23 ++- src/utils/sequentialPromiseMap.ts | 13 -- 39 files changed, 749 insertions(+), 357 deletions(-) create mode 100644 src/extensions/executor.ts create mode 100644 src/templates/common/triggerEvents.ejs delete mode 100644 src/utils/sequentialPromiseMap.ts diff --git a/__tests__/cli.test.ts b/__tests__/cli.test.ts index e90a9c8..62c7816 100644 --- a/__tests__/cli.test.ts +++ b/__tests__/cli.test.ts @@ -17,15 +17,16 @@ test('prints help', async () => { '--help', '--version', '--skip-git-check', - '--preset', - 'Use any combination of the following with --preset flag to specify your own set of workflows to generate', - '--lint', - '--jest', - '--ts', - '--prettier', - '--eas', - '--detox', - '--maestro', + '-pull-request [...workflows]', + '-main [...workflows]', + 'Use any combination of the following with --pull-request and --main flags to specify your own set of workflows to generate', + 'lint', + 'jest', + 'ts', + 'prettier', + 'eas', + 'detox', + 'maestro', ]) { expect(output).toContain(message) } diff --git a/__tests__/recipes/detox.test.ts b/__tests__/recipes/detox.test.ts index 5f0ef33..7791d5e 100644 --- a/__tests__/recipes/detox.test.ts +++ b/__tests__/recipes/detox.test.ts @@ -5,7 +5,7 @@ import { getPackageJsonWithoutVersions, installDependencies, NON_INTERACTIVE_FLAG, - PRESET_FLAG, + PULL_REQUEST_FLAG, removeTestProject, setupTestProject, TEST_PROJECTS, @@ -75,7 +75,7 @@ describe('detox recipe', () => { TEST_PROJECTS[projectName] const output = await cli( - [PRESET_FLAG, NON_INTERACTIVE_FLAG, `--${FLAG}`], + [PULL_REQUEST_FLAG, FLAG, NON_INTERACTIVE_FLAG], { cwd: appRoot, } @@ -88,12 +88,12 @@ describe('detox recipe', () => { 'https://wix.github.io/Detox/docs/next/introduction/project-setup/#step-4-additional-android-configuration.', 'You can do it now or after the script finishes.', `Detected ${packageManager} as your package manager.`, - 'Generating Detox workflow', + 'Configuring project for Detox', 'Created Android release build workflow.', 'Created iOS release build workflow.', 'Consider adding "modulePathIgnorePatterns": ["e2e"] to your jest config.', 'Remember to edit example test in e2e/starter.test.ts to match your app.', - 'Created Detox workflow.', + 'Created Detox workflow for events: [pull_request]', 'Follow Step 4 of https://wix.github.io/Detox/docs/next/introduction/project-setup/#step-4-additional-android-configuration to patch native code for Detox.', ]) { expect(output).toContain(message) @@ -116,20 +116,20 @@ describe('detox recipe', () => { installDependencies(appRoot, packageManager) - const output = await cli([PRESET_FLAG, `--${FLAG}`], { + const output = await cli([PULL_REQUEST_FLAG, FLAG], { cwd: appRoot, }) for (const message of [ `Detected ${packageManager} as your package manager.`, - 'Generating Detox workflow', + 'Configuring project for Detox', 'Running Expo prebuild...', 'Finished running Expo prebuild.', 'Created Android release build workflow.', 'Created iOS release build workflow.', 'Consider adding "modulePathIgnorePatterns": ["e2e"] to your jest config.', 'Remember to edit example test in e2e/starter.test.ts to match your app.', - 'Created Detox workflow.', + 'Created Detox workflow for events: [pull_request]', ]) { expect(output).toContain(message) } diff --git a/__tests__/recipes/jest.test.ts b/__tests__/recipes/jest.test.ts index 564025d..a43ea42 100644 --- a/__tests__/recipes/jest.test.ts +++ b/__tests__/recipes/jest.test.ts @@ -3,7 +3,7 @@ import { join } from 'path' import { cli, getPackageJsonWithoutVersions, - PRESET_FLAG, + PULL_REQUEST_FLAG, removeTestProject, setupTestProject, TEST_PROJECTS, @@ -30,14 +30,14 @@ describe('jest recipe', () => { const { packageManager, repoRoot, appRoot, workflowNamePrefix } = TEST_PROJECTS[projectName] - const output = await cli([PRESET_FLAG, `--${FLAG}`], { + const output = await cli([PULL_REQUEST_FLAG, FLAG], { cwd: appRoot, }) for (const message of [ `Detected ${packageManager} as your package manager.`, - 'Generating Jest workflow', - 'Created Jest workflow.', + 'Configuring project for Jest', + 'Created Jest workflow for events: [pull_request]', ]) { expect(output).toContain(message) } diff --git a/__tests__/recipes/lint.test.ts b/__tests__/recipes/lint.test.ts index 5976bc1..ed34cbc 100644 --- a/__tests__/recipes/lint.test.ts +++ b/__tests__/recipes/lint.test.ts @@ -3,7 +3,7 @@ import { join } from 'path' import { cli, getPackageJsonWithoutVersions, - PRESET_FLAG, + PULL_REQUEST_FLAG, removeTestProject, setupTestProject, TEST_PROJECTS, @@ -37,14 +37,14 @@ describe('lint recipe', () => { existingConfig, } = TEST_PROJECTS[projectName] - const output = await cli([PRESET_FLAG, `--${FLAG}`], { + const output = await cli([PULL_REQUEST_FLAG, FLAG], { cwd: appRoot, }) for (const message of [ `Detected ${packageManager} as your package manager.`, - 'Generating ESLint workflow', - 'Created ESLint workflow.', + 'Configuring project for ESLint', + 'Created ESLint workflow for events: [pull_request]', ]) { expect(output).toContain(message) } @@ -75,11 +75,13 @@ describe('lint recipe', () => { setupTestProject('rn-setup-ci-yarn-flat') const { appRoot } = TEST_PROJECTS['rn-setup-ci-yarn-flat'] - const output = await cli([PRESET_FLAG, `--${FLAG}`, `--${PRETTIER_FLAG}`], { + const output = await cli([PULL_REQUEST_FLAG, FLAG, PRETTIER_FLAG], { cwd: appRoot, }) - expect(output).toContain('Created ESLint workflow.') + expect(output).toContain( + 'Created ESLint workflow for events: [pull_request]' + ) const packageJson = await getPackageJsonWithoutVersions( join(appRoot, 'package.json') diff --git a/__tests__/recipes/prettier.test.ts b/__tests__/recipes/prettier.test.ts index 9723b63..762168b 100644 --- a/__tests__/recipes/prettier.test.ts +++ b/__tests__/recipes/prettier.test.ts @@ -3,7 +3,7 @@ import { join } from 'path' import { cli, getPackageJsonWithoutVersions, - PRESET_FLAG, + PULL_REQUEST_FLAG, removeTestProject, setupTestProject, TEST_PROJECTS, @@ -35,14 +35,14 @@ describe('prettier check recipe', () => { existingConfig, } = TEST_PROJECTS[projectName] - const output = await cli([PRESET_FLAG, `--${FLAG}`], { + const output = await cli([PULL_REQUEST_FLAG, FLAG], { cwd: appRoot, }) for (const message of [ `Detected ${packageManager} as your package manager.`, - 'Generating Prettier check workflow', - 'Created Prettier check workflow.', + 'Configuring project for Prettier check', + 'Created Prettier check workflow for events: [pull_request]', ]) { expect(output).toContain(message) } diff --git a/__tests__/recipes/typescript.test.ts b/__tests__/recipes/typescript.test.ts index f0054e2..555050d 100644 --- a/__tests__/recipes/typescript.test.ts +++ b/__tests__/recipes/typescript.test.ts @@ -3,7 +3,7 @@ import { join } from 'path' import { cli, getPackageJsonWithoutVersions, - PRESET_FLAG, + PULL_REQUEST_FLAG, removeTestProject, setupTestProject, TEST_PROJECTS, @@ -30,14 +30,14 @@ describe('typescript check recipe', () => { const { packageManager, repoRoot, appRoot, workflowNamePrefix } = TEST_PROJECTS[projectName] - const output = await cli([PRESET_FLAG, `--${FLAG}`], { + const output = await cli([PULL_REQUEST_FLAG, FLAG], { cwd: appRoot, }) for (const message of [ `Detected ${packageManager} as your package manager.`, - 'Generating Typescript check workflow', - 'Created Typescript check workflow.', + 'Configuring project for Typescript check', + 'Created Typescript check workflow for events: [pull_request]', ]) { expect(output).toContain(message) } diff --git a/__tests__/runtime-version-file.test.ts b/__tests__/runtime-version-file.test.ts index 0ff9bcb..8ec4440 100644 --- a/__tests__/runtime-version-file.test.ts +++ b/__tests__/runtime-version-file.test.ts @@ -2,6 +2,7 @@ import { join } from 'path' import { readFileSync } from 'fs' import { cli, + PULL_REQUEST_FLAG, removeTestProject, setupTestProject, TEST_PROJECTS, @@ -17,7 +18,7 @@ describe('should create runtime version files if necessary', () => { const { appRoot } = TEST_PROJECTS['rn-setup-ci-create-expo-stack'] setupTestProject('rn-setup-ci-create-expo-stack') - const output = await cli(['--skip-git-check', '--preset', '--lint'], { + const output = await cli(['--skip-git-check', PULL_REQUEST_FLAG, 'lint'], { cwd: appRoot, }) @@ -43,7 +44,7 @@ describe('should create runtime version files if necessary', () => { const { appRoot } = TEST_PROJECTS['rn-setup-ci-create-expo-stack-bun'] setupTestProject('rn-setup-ci-create-expo-stack-bun') - const output = await cli(['--skip-git-check', '--preset', '--lint'], { + const output = await cli(['--skip-git-check', PULL_REQUEST_FLAG, 'lint'], { cwd: appRoot, }) diff --git a/__tests__/utils.ts b/__tests__/utils.ts index 8f83b87..b5c5f2f 100644 --- a/__tests__/utils.ts +++ b/__tests__/utils.ts @@ -10,7 +10,7 @@ export const PATH_TO_TEST_PROJECTS = join(__dirname, TEST_PROJECTS_FOLDER) const PATH_TO_TEST_PROJECT = join(__dirname, TEST_PROJECT_NAME) export const NON_INTERACTIVE_FLAG = '--non-interactive' -export const PRESET_FLAG = '--preset' +export const PULL_REQUEST_FLAG = '-pull-request' const INSTALL_DEPENDENCIES_COMMAND = { yarn: 'yarn', diff --git a/src/commands/help.ts b/src/commands/help.ts index d2668cc..0df03da 100644 --- a/src/commands/help.ts +++ b/src/commands/help.ts @@ -1,6 +1,6 @@ import { GluegunCommand, GluegunToolbox } from 'gluegun' import { CycliCommand } from './setup-ci' -import { HELP_FLAG } from '../constants' +import { HELP_FLAG, MAIN_FLAG, PULL_REQUEST_FLAG } from '../constants' module.exports = { name: HELP_FLAG, @@ -28,14 +28,19 @@ module.exports = { const maxFlagLength = Math.max( ...[...cycliCommand.options, ...cycliCommand.featureOptions].map( - (op) => op.flag.length + 2 + (op) => op.flag.length + 2 + (op.params ? '[...workflows]'.length : 0) ) ) for (const option of cycliCommand.options) { toolbox.interactive.info( ' ' + - cyan(`--${option.flag}`.padEnd(maxFlagLength + 2, ' ')) + + cyan( + (option.params + ? `-${option.flag} [...workflows]` + : `--${option.flag}` + ).padEnd(maxFlagLength + 2, ' ') + ) + '\t' + option.description ) @@ -43,15 +48,15 @@ module.exports = { interactive.vspace() interactive.info( - 'Use any combination of the following with --preset flag to specify your own set of workflows to generate' + `Use any combination of the following with -${PULL_REQUEST_FLAG} and -${MAIN_FLAG} flags to specify your own set of workflows to generate` ) interactive.vspace() - interactive.info(bold(underline('Feature flags:'))) + interactive.info(bold(underline('Available workflows:'))) for (const option of cycliCommand.featureOptions) { toolbox.interactive.info( ' ' + - cyan(`--${option.flag}`.padEnd(maxFlagLength + 2, ' ')) + + cyan(option.flag.padEnd(maxFlagLength + 2, ' ')) + '\t' + option.description ) diff --git a/src/commands/setup-ci.ts b/src/commands/setup-ci.ts index 631ae35..861d0c2 100644 --- a/src/commands/setup-ci.ts +++ b/src/commands/setup-ci.ts @@ -1,19 +1,13 @@ import { GluegunCommand, GluegunToolbox } from 'gluegun' -import lint from '../recipes/lint' -import jest from '../recipes/jest' -import typescriptCheck from '../recipes/typescript' -import prettierCheck from '../recipes/prettier' -import eas from '../recipes/eas' -import detox from '../recipes/detox' -import maestro from '../recipes/maestro' import isGitDirty from 'is-git-dirty' -import sequentialPromiseMap from '../utils/sequentialPromiseMap' import { CycliError, CycliRecipe, CycliToolbox } from '../types' import { COLORS, CYCLI_COMMAND, HELP_FLAG, - PRESET_FLAG, + MAIN_FLAG, + PULL_REQUEST_FLAG, + RECIPES, REPOSITORY_METRICS_HELP_URL, REPOSITORY_TROUBLESHOOTING_URL, REPOSITORY_URL, @@ -25,7 +19,7 @@ const FEEDBACK_SURVEY_URL = 'https://forms.gle/NYoPyPxnVzGheHcw6' const SKIP_GIT_CHECK_FLAG = 'skip-git-check' -type Option = { flag: string; description: string } +type Option = { flag: string; description: string; params: boolean } export type CycliCommand = GluegunCommand & { description: string @@ -33,29 +27,15 @@ export type CycliCommand = GluegunCommand & { featureOptions: Option[] } -const RECIPES = [ - lint, - jest, - typescriptCheck, - prettierCheck, - eas, - detox, - maestro, -] - const runReactNativeCiCli = async (toolbox: CycliToolbox) => { const snapshotBefore = await toolbox.diff.gitStatus() toolbox.interactive.surveyStep( 'Created snapshot of project state before execution.' ) - await toolbox.config.prompt(RECIPES) - - const executors = RECIPES.filter((recipe: CycliRecipe) => - toolbox.config.getSelectedRecipes().includes(recipe.meta.flag) - ).map((recipe: CycliRecipe) => recipe.execute) + await toolbox.config.get() - if (executors.length === 0) { + if ((await toolbox.config.getSelectedRecipes()).size === 0) { toolbox.interactive.outro('Nothing to do here. Cheers! 🎉') return } @@ -64,7 +44,8 @@ const runReactNativeCiCli = async (toolbox: CycliToolbox) => { `Detected ${toolbox.context.packageManager()} as your package manager.` ) - await sequentialPromiseMap(executors, (executor) => executor(toolbox)) + await toolbox.executor.configureProject() + await toolbox.executor.generateWorkflows() const snapshotAfter = await toolbox.diff.gitStatus() const diff = toolbox.diff.compare(snapshotBefore, snapshotAfter) @@ -74,18 +55,23 @@ const runReactNativeCiCli = async (toolbox: CycliToolbox) => { toolbox.furtherActions.print() - const usedFlags = toolbox.config - .getSelectedRecipes() - .map((flag: string) => `--${flag}`) - .join(' ') - toolbox.interactive.vspace() toolbox.interactive.success(`We're all set 🎉`) if (!toolbox.options.isPreset()) { - toolbox.interactive.success( - `Next time you can specify a preset to reproduce this run using npx ${CYCLI_COMMAND} --${PRESET_FLAG} ${usedFlags}.` - ) + const pullRequestRecipes = toolbox.config.getPullRequestRecipes() + const mainRecipes = toolbox.config.getMainRecipes() + + let presetMessage = `Next time you can specify a preset to reproduce this run using npx ${CYCLI_COMMAND}` + + if (pullRequestRecipes.length > 0) { + presetMessage += ` -${PULL_REQUEST_FLAG} ${pullRequestRecipes.join(' ')}` + } + if (mainRecipes.length > 0) { + presetMessage += ` -${MAIN_FLAG} ${mainRecipes.join(' ')}` + } + + toolbox.interactive.success(presetMessage) } toolbox.interactive.vspace() @@ -95,7 +81,7 @@ const runReactNativeCiCli = async (toolbox: CycliToolbox) => { `Thank you for using ${COLORS.cyan('setup-ci')} 💙`, "We'd love to hear your feedback to make it even better.", 'Please take a moment to fill out our survey:\n', - `\t → ${FEEDBACK_SURVEY_URL}\n`, + `\t → ${FEEDBACK_SURVEY_URL} \n`, 'Your input is greatly appreciated! 🙏', ].join('\n'), 'green' @@ -115,7 +101,7 @@ const checkGit = async (toolbox: CycliToolbox) => { } else { if (toolbox.options.isPreset()) { throw CycliError( - `You have to commit your changes before running with preset or use --${SKIP_GIT_CHECK_FLAG}.` + `You have to commit your changes before running with preset or use--${SKIP_GIT_CHECK_FLAG}.` ) } @@ -151,9 +137,9 @@ const run = async (toolbox: CycliToolbox) => { `${COLORS.cyan( `npx ${CYCLI_COMMAND}` )} aims to help you set up CI workflows for your React Native app.`, - `If you find the project useful, you can give us a ⭐ on GitHub:`, + `If you find the project useful, you can give us a ⭐ on GitHub: `, '', - `\t\t → ${REPOSITORY_URL}`, + `\t\t → ${REPOSITORY_URL} `, ].join('\n'), 'green' ) @@ -161,8 +147,8 @@ const run = async (toolbox: CycliToolbox) => { if (!toolbox.options.skipTelemetry()) { toolbox.interactive.surveyInfo( [ - `This script collects anonymous usage data. You can disable it by using --skip-telemetry.`, - `Learn more at ${REPOSITORY_METRICS_HELP_URL}`, + `This script collects anonymous usage data.You can disable it by using--skip - telemetry.`, + `Learn more at ${REPOSITORY_METRICS_HELP_URL} `, ].join('\n'), 'dim' ) @@ -182,7 +168,7 @@ const run = async (toolbox: CycliToolbox) => { if (!isCycliError(error)) { errMessage = [ - `${CYCLI_COMMAND} failed with unexpected error:`, + `${CYCLI_COMMAND} failed with unexpected error: `, errMessage, `You can check ${REPOSITORY_TROUBLESHOOTING_URL} for potential solution.`, ].join('\n') @@ -194,14 +180,16 @@ const run = async (toolbox: CycliToolbox) => { } try { + const selectedRecipes = await toolbox.config.getSelectedRecipes() + if (!toolbox.options.skipTelemetry()) { await toolbox.telemetry.sendLog({ version: toolbox.meta.version(), firstUse: toolbox.context.isFirstUse(), options: Object.fromEntries( - RECIPES.map((recipe) => [ + Object.values(RECIPES).map((recipe) => [ recipe.meta.flag, - toolbox.config.getSelectedRecipes().includes(recipe.meta.flag), + selectedRecipes.has(recipe.meta.flag), ]) ), error: finishedWithUnexpectedError, @@ -215,9 +203,10 @@ const run = async (toolbox: CycliToolbox) => { } const getFeatureOptions = (): Option[] => { - return RECIPES.map((recipe: CycliRecipe) => ({ + return Object.values(RECIPES).map((recipe: CycliRecipe) => ({ flag: recipe.meta.flag, description: recipe.meta.description, + params: false, })) } @@ -225,20 +214,28 @@ const command: CycliCommand = { name: CYCLI_COMMAND, description: 'Quickly setup CI workflows for your React Native app', options: [ - { flag: HELP_FLAG, description: 'Print help message' }, - { flag: 'version', description: 'Print version' }, + { flag: HELP_FLAG, description: 'Print help message', params: false }, + { flag: 'version', description: 'Print version', params: false }, { flag: SKIP_GIT_CHECK_FLAG, description: 'Skip check for dirty git repository', - }, - { - flag: PRESET_FLAG, - description: - 'Run with preset. Combine with feature flags to specify generated workflows', + params: false, }, { flag: SKIP_TELEMETRY_FLAG, description: 'Skip telemetry data collection', + params: false, + }, + { + flag: PULL_REQUEST_FLAG, + description: 'Specify workflows to generate to run on every pull request', + params: true, + }, + { + flag: MAIN_FLAG, + description: + 'Specify workflows to generate to run on every push to the main branch', + params: true, }, ], featureOptions: [...getFeatureOptions()], diff --git a/src/constants.ts b/src/constants.ts index 596d69c..4ae60ea 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,7 +1,25 @@ import { print } from 'gluegun' +import { CycliRecipe, CycliRecipeType } from './types' +import lint from './recipes/lint' +import jest from './recipes/jest' +import typescriptCheck from './recipes/typescript' +import prettierCheck from './recipes/prettier' +import eas from './recipes/eas' +import detox from './recipes/detox' +import maestro from './recipes/maestro' export const CYCLI_COMMAND = 'setup-ci' +export const RECIPES: Record = { + [CycliRecipeType.ESLINT]: lint, + [CycliRecipeType.JEST]: jest, + [CycliRecipeType.TYPESCRIPT]: typescriptCheck, + [CycliRecipeType.PRETTIER]: prettierCheck, + [CycliRecipeType.EAS]: eas, + [CycliRecipeType.DETOX]: detox, + [CycliRecipeType.MAESTRO]: maestro, +} as const + export const CYCLI_ERROR_NAME = 'CycliError' export const COLORS = { @@ -28,7 +46,7 @@ export const S_SUCCESS = COLORS.green('◆') export const S_CONFIRM = COLORS.magenta('◆') export const S_ACTION = COLORS.cyan('▼') export const S_ACTION_BULLET = COLORS.cyan('►') -export const S_MULTISELECT_MESSAGE = COLORS.blue('◆') +export const S_MULTISELECT_MESSAGE = '◆' export const S_BAR = '│' export const S_VBAR = '─' @@ -43,7 +61,8 @@ export const S_R_ARROW = '►' export const NON_INTERACTIVE_FLAG = 'non-interactive' export const HELP_FLAG = 'help' -export const PRESET_FLAG = 'preset' +export const PULL_REQUEST_FLAG = 'pull-request' +export const MAIN_FLAG = 'main' export const SKIP_TELEMETRY_FLAG = 'skip-telemetry' export const LOCK_FILE_TO_MANAGER = { diff --git a/src/extensions/config.ts b/src/extensions/config.ts index 9df5b56..54f5f6b 100644 --- a/src/extensions/config.ts +++ b/src/extensions/config.ts @@ -1,81 +1,170 @@ import { + CycliConfig, CycliError, CycliRecipe, CycliRecipeType, CycliToolbox, + WorkflowEventType, } from '../types' -import intersection from 'lodash/intersection' import { messageFromError } from '../utils/errors' -import { DOCS_WORKFLOWS_URL } from '../constants' +import { DOCS_WORKFLOWS_URL, RECIPES } from '../constants' module.exports = (toolbox: CycliToolbox) => { // State for caching the config - let selectedRecipes: CycliRecipeType[] | undefined = undefined + let config: CycliConfig | undefined = undefined + + const recipeToMultiselectOption = ({ + validate, + meta: { name, flag, selectHint }, + }: CycliRecipe) => { + let validationError = '' + try { + validate?.(toolbox) + } catch (error: unknown) { + validationError = messageFromError(error) + } + const hint = validationError || selectHint + const disabled = Boolean(validationError) + return { + label: name, + value: flag, + hint, + disabled, + } + } + + const prompt = async (): Promise => { + let selectedPullRequestRecipes: CycliRecipeType[] = [] + let selectedMainRecipes: CycliRecipeType[] = [] - const prompt = async (allRecipes: CycliRecipe[]): Promise => { if (toolbox.options.isPreset()) { - const allFlags = Object.values(CycliRecipeType) - - selectedRecipes = intersection( - allFlags, - Object.keys(toolbox.parameters.options) - .filter((option) => allFlags.includes(option as CycliRecipeType)) - .map((flag) => flag as CycliRecipeType) - ) - - allRecipes.forEach((recipe: CycliRecipe) => { - if (selectedRecipes?.includes(recipe.meta.flag)) { - try { - recipe.validate?.(toolbox) - } catch (error: unknown) { - const validationError = messageFromError(error) - - // adding context to validation error reason (used in multiselect menu hint) - throw CycliError( - `Cannot generate ${recipe.meta.name} workflow in your project.\nReason: ${validationError}` - ) - } + selectedPullRequestRecipes = toolbox.options.pullRequestRecipes() + selectedMainRecipes = toolbox.options.mainRecipes() + const selectedRecipes = [ + ...selectedMainRecipes, + ...selectedPullRequestRecipes, + ] + + selectedRecipes.forEach((recipe: CycliRecipeType) => { + try { + RECIPES[recipe].validate?.(toolbox) + } catch (error: unknown) { + const validationError = messageFromError(error) + + // adding context to validation error reason (used in multiselect menu hint) + throw CycliError( + `Cannot generate ${RECIPES[recipe].meta.name} workflow in your project.\nReason: ${validationError}` + ) + } + }) + + selectedPullRequestRecipes.forEach((recipe: CycliRecipeType) => { + if ( + !RECIPES[recipe].meta.allowedEvents.includes( + WorkflowEventType.PULL_REQUEST + ) + ) { + throw CycliError( + `Cannot generate ${RECIPES[recipe].meta.name} workflow for pull requests.` + ) + } + }) + + selectedMainRecipes.forEach((recipe: CycliRecipeType) => { + if ( + !RECIPES[recipe].meta.allowedEvents.includes(WorkflowEventType.PUSH) + ) { + throw CycliError( + `Cannot generate ${RECIPES[recipe].meta.name} workflow for push to main branch.` + ) } }) } else { - selectedRecipes = (await toolbox.interactive.multiselect( + selectedPullRequestRecipes = (await toolbox.interactive.multiselect( 'Select workflows you want to run on every PR', - `Learn more about PR workflows: ${DOCS_WORKFLOWS_URL}`, - allRecipes.map( - ({ validate, meta: { name, flag, selectHint } }: CycliRecipe) => { - let validationError = '' - try { - validate?.(toolbox) - } catch (error: unknown) { - validationError = messageFromError(error) - } - const hint = validationError || selectHint - const disabled = Boolean(validationError) - return { - label: name, - value: flag, - hint, - disabled, - } - } - ) + `Learn more about available workflows: ${DOCS_WORKFLOWS_URL}`, + Object.values(RECIPES) + .filter((recipe) => + recipe.meta.allowedEvents.includes(WorkflowEventType.PULL_REQUEST) + ) + .map(recipeToMultiselectOption), + false, + 'blue' + )) as CycliRecipeType[] + + selectedMainRecipes = (await toolbox.interactive.multiselect( + 'Select workflows you want to run on every push to main branch', + `Learn more about available workflows: ${DOCS_WORKFLOWS_URL}`, + Object.values(RECIPES) + .filter((recipe) => + recipe.meta.allowedEvents.includes(WorkflowEventType.PUSH) + ) + .map(recipeToMultiselectOption), + false, + 'magenta' )) as CycliRecipeType[] } + return new Map([ + [ + { type: WorkflowEventType.PULL_REQUEST }, + new Set(selectedPullRequestRecipes), + ], + [ + { type: WorkflowEventType.PUSH, branch: 'main' }, + new Set(selectedMainRecipes), + ], + ]) + } + + const getPullRequestRecipes = (): CycliRecipeType[] => { + return Array.from( + Array.from(config?.entries() ?? []).find( + ([event]) => event.type === WorkflowEventType.PULL_REQUEST + )?.[1] ?? [] + ) + } + + const getMainRecipes = (): CycliRecipeType[] => { + return Array.from( + Array.from(config?.entries() ?? []).find( + ([event]) => event.type === WorkflowEventType.PUSH + )?.[1] ?? [] + ) + } + + const getSelectedRecipes = async (): Promise> => { + if (config === undefined) { + throw Error('Config not initialized') + } + + return new Set( + Array.from(config.values()) + .map((s) => Array.from(s)) + .flat() + ) } - const getSelectedRecipes = (): CycliRecipeType[] => { - return selectedRecipes || [] + const get = async (): Promise => { + if (config === undefined) { + config = await prompt() + } + return config } toolbox.config = { prompt, + getPullRequestRecipes, + getMainRecipes, getSelectedRecipes, + get, } } export interface ConfigExtension { config: { - prompt: (allRecipes: CycliRecipe[]) => Promise - getSelectedRecipes: () => CycliRecipeType[] + get: () => Promise + getPullRequestRecipes: () => CycliRecipeType[] + getMainRecipes: () => CycliRecipeType[] + getSelectedRecipes: () => Promise> } } diff --git a/src/extensions/executor.ts b/src/extensions/executor.ts new file mode 100644 index 0000000..e8c01f3 --- /dev/null +++ b/src/extensions/executor.ts @@ -0,0 +1,45 @@ +import { RECIPES } from '../constants' +import { CycliToolbox } from '../types' + +module.exports = (toolbox: CycliToolbox) => { + const configureProject = async () => { + const selectedRecipes = Array.from( + await toolbox.config.getSelectedRecipes() + ) + + for (const recipeType of selectedRecipes) { + await RECIPES[recipeType].configureProject(toolbox) + } + } + + const generateWorkflows = async () => { + toolbox.interactive.vspace() + toolbox.interactive.sectionHeader('Generating workflows') + + const selectedRecipes = Array.from( + await toolbox.config.getSelectedRecipes() + ) + const config = await toolbox.config.get() + + for (const recipeType of selectedRecipes) { + await RECIPES[recipeType].generateWorkflow( + toolbox, + Array.from(config) + .filter(([, recipes]) => recipes.has(recipeType)) + .map(([event]) => event) + ) + } + } + + toolbox.executor = { + configureProject, + generateWorkflows, + } +} + +export interface ExecutorExtension { + executor: { + configureProject: () => Promise + generateWorkflows: () => Promise + } +} diff --git a/src/extensions/interactive.ts b/src/extensions/interactive.ts index 4e4b33e..b04b45c 100644 --- a/src/extensions/interactive.ts +++ b/src/extensions/interactive.ts @@ -42,7 +42,6 @@ interface Spinner { module.exports = (toolbox: CycliToolbox) => { const { - blue, bgWhite, bold, cyan, @@ -242,8 +241,17 @@ module.exports = (toolbox: CycliToolbox) => { const multiselect = async ( message: string, hint: string, - options: { label: string; value: string; hint: string; disabled: boolean }[] + options: { + label: string + value: string + hint: string + disabled: boolean + }[], + required: boolean, + accentColor: MessageColor ): Promise => { + const accent = COLORS[accentColor] + const opt = ( option: { label: string @@ -261,16 +269,18 @@ module.exports = (toolbox: CycliToolbox) => { switch (state) { case 'active': { - return `${blue(S_RADIO_INACTIVE)} ${bold(label)} ${dim(`(${hint})`)}` + return `${accent(S_RADIO_INACTIVE)} ${bold(label)} ${dim( + `(${hint})` + )}` } case 'selected': { - return `${blue(S_RADIO_ACTIVE)} ${dim(label)}` + return `${accent(S_RADIO_ACTIVE)} ${dim(label)}` } case 'active-selected': { - return `${blue(S_RADIO_ACTIVE)} ${label} ${dim(`(${hint})`)}` + return `${accent(S_RADIO_ACTIVE)} ${label} ${dim(`(${hint})`)}` } case 'inactive': { - return `${dim(blue(S_RADIO_INACTIVE))} ${dim(label)}` + return `${dim(accent(S_RADIO_INACTIVE))} ${dim(label)}` } } } @@ -288,17 +298,19 @@ module.exports = (toolbox: CycliToolbox) => { const multiselectPromise = new MultiSelectPrompt({ options: enabledOptions, initialValues: [], - required: true, + required, cursorAt: options[0].value, validate(selected: string[]) { if (this.required && selected.length === 0) return 'Please select at least one option.' }, render() { - const title = `${gray(S_BAR)}\n${S_MULTISELECT_MESSAGE} ${bold( + const title = `${gray(S_BAR)}\n${accent(S_MULTISELECT_MESSAGE)} ${bold( message )}\n${ - ['submit', 'cancel'].includes(this.state) ? gray(S_BAR) : blue(S_BAR) + ['submit', 'cancel'].includes(this.state) + ? gray(S_BAR) + : accent(S_BAR) } ${dim(hint)}\n` const styleOption = ( @@ -322,8 +334,8 @@ module.exports = (toolbox: CycliToolbox) => { const optionsList = title + - `${blue(S_BAR)}\n` + - blue(S_BAR) + + `${accent(S_BAR)}\n` + + accent(S_BAR) + ' ' + options .map((option) => { @@ -333,14 +345,14 @@ module.exports = (toolbox: CycliToolbox) => { } return styleOption(option, this.cursor === indexInEnabled) }) - .join(`\n${blue(S_BAR)} `) + + .join(`\n${accent(S_BAR)} `) + '\n' const selectedOptions = this.options.filter(({ value }) => this.value.includes(value) ) - const selectedInfo = `${blue(S_R_ARROW)} ${dim( + const selectedInfo = `${accent(S_R_ARROW)} ${dim( `Selected: ${selectedOptions .map((option) => option.label) .join(', ')}` @@ -369,14 +381,14 @@ module.exports = (toolbox: CycliToolbox) => { ) .join('\n') - return `${optionsList}${blue(S_BAR)} \n${blue( + return `${optionsList}${accent(S_BAR)} \n${accent( S_BAR - )} ${footer} \n${blue(S_BAR_END)} \n${instruction} ` + )} ${footer} \n${accent(S_BAR_END)} \n${instruction} ` } default: { - return `${optionsList}${blue(S_BAR)} \n${blue( + return `${optionsList}${accent(S_BAR)} \n${accent( S_BAR - )} ${selectedInfo} \n${blue(S_BAR_END)} \n${instruction} ` + )} ${selectedInfo} \n${accent(S_BAR_END)} \n${instruction} ` } } }, @@ -551,7 +563,9 @@ export interface InteractiveExtension { value: string hint: string disabled: boolean - }[] + }[], + required: boolean, + accentColor: MessageColor ) => Promise confirm: ( message: string, diff --git a/src/extensions/options.ts b/src/extensions/options.ts index 198e76e..77e2535 100644 --- a/src/extensions/options.ts +++ b/src/extensions/options.ts @@ -1,16 +1,53 @@ import { + MAIN_FLAG, NON_INTERACTIVE_FLAG, - PRESET_FLAG, + PULL_REQUEST_FLAG, SKIP_TELEMETRY_FLAG, } from '../constants' -import { CycliToolbox } from '../types' +import { CycliError, CycliRecipeType, CycliToolbox } from '../types' module.exports = (toolbox: CycliToolbox) => { - const isPreset = () => Boolean(toolbox.parameters.options[PRESET_FLAG]) + const getListOfParameters = (key: string): string[] => { + const argv = toolbox.parameters.argv - const isRecipeSelected = (recipeFlag: string) => - Boolean(toolbox.parameters.options[PRESET_FLAG]) && - Boolean(toolbox.parameters.options[recipeFlag]) + const pullRequestFlagIdx = argv.findIndex( + (arg: string) => arg === `-${key}` + ) + + if (pullRequestFlagIdx === -1) { + return [] + } + + let lastPullRequestRecipeIdx = argv.findIndex( + (arg: string, idx: number) => + idx > pullRequestFlagIdx && arg.startsWith('-') + ) + + if (lastPullRequestRecipeIdx === -1) { + lastPullRequestRecipeIdx = argv.length + } + + return argv.slice(pullRequestFlagIdx + 1, lastPullRequestRecipeIdx) + } + + const validateOnlyKnownRecipes = (recipes: string[]): CycliRecipeType[] => { + recipes.forEach((recipe) => { + if (!(Object.values(CycliRecipeType) as string[]).includes(recipe)) { + throw CycliError(`Unknown recipe: ${recipe}`) + } + }) + return recipes as CycliRecipeType[] + } + + const isPreset = () => + Boolean(toolbox.parameters.argv.includes(`-${PULL_REQUEST_FLAG}`)) || + Boolean(toolbox.parameters.argv.includes(`-${MAIN_FLAG}`)) + + const pullRequestRecipes = (): CycliRecipeType[] => + validateOnlyKnownRecipes(getListOfParameters(PULL_REQUEST_FLAG)) + + const mainRecipes = (): CycliRecipeType[] => + validateOnlyKnownRecipes(getListOfParameters(MAIN_FLAG)) const isNonInteractive = () => Boolean(toolbox.parameters.options[NON_INTERACTIVE_FLAG]) @@ -20,7 +57,8 @@ module.exports = (toolbox: CycliToolbox) => { toolbox.options = { isPreset, - isRecipeSelected, + pullRequestRecipes, + mainRecipes, isNonInteractive, skipTelemetry, } @@ -29,7 +67,8 @@ module.exports = (toolbox: CycliToolbox) => { export interface OptionsExtension { options: { isPreset: () => boolean - isRecipeSelected: (recipeFlag: string) => boolean + pullRequestRecipes: () => CycliRecipeType[] + mainRecipes: () => CycliRecipeType[] isNonInteractive: () => boolean skipTelemetry: () => boolean } diff --git a/src/extensions/projectConfig.ts b/src/extensions/projectConfig.ts index 4ffec2d..aeb3a17 100644 --- a/src/extensions/projectConfig.ts +++ b/src/extensions/projectConfig.ts @@ -11,6 +11,10 @@ const DEFAULT_BUN_VERSION = '1.1.30' module.exports = (toolbox: CycliToolbox) => { const { filesystem } = toolbox + // State for caching the config + let expo: boolean | undefined = undefined + let iOSAppName: string | undefined = undefined + const packageJson = (): PackageJson => { if (!filesystem.exists('package.json')) { throw CycliError( @@ -173,19 +177,20 @@ module.exports = (toolbox: CycliToolbox) => { } const isExpo = (): boolean => { - const appConfig = appJson() - - if (appConfig?.expo) { - return true + if (expo !== undefined) { + return expo } - const dynamicAppConfig = appJs() + const appConfig = appJson() - if (dynamicAppConfig?.includes('expo:')) { - return true + if (appConfig?.expo) { + expo = true + } else { + const dynamicAppConfig = appJs() + expo = dynamicAppConfig?.includes('expo:') || false } - return false + return expo } const getName = (): string => { @@ -273,6 +278,35 @@ module.exports = (toolbox: CycliToolbox) => { } } + const getIOSAppName = async (): Promise => { + if (iOSAppName != undefined) { + return iOSAppName + } + + const existsAndroidDir = toolbox.filesystem.exists('android') + const existsIOsDir = toolbox.filesystem.exists('ios') + + if (isExpo()) { + await toolbox.expo.prebuild({ cleanAfter: false }) + } + + iOSAppName = toolbox.filesystem + .list('ios') + ?.find((file) => file.endsWith('.xcodeproj')) + ?.replace('.xcodeproj', '') + + if (!iOSAppName) { + throw CycliError( + 'Failed to obtain iOS app name. Perhaps your ios/ directory is missing *.xcodeproj file.' + ) + } + + if (!existsAndroidDir) toolbox.filesystem.remove('android') + if (!existsIOsDir) toolbox.filesystem.remove('ios') + + return iOSAppName + } + toolbox.projectConfig = { packageJson, appJsonFile, @@ -285,6 +319,7 @@ module.exports = (toolbox: CycliToolbox) => { getName, getAppId, checkAppNameInConfigOrGenerate, + getIOSAppName, } } @@ -304,5 +339,6 @@ export interface ProjectConfigExtension { getName: () => string getAppId: () => string | undefined checkAppNameInConfigOrGenerate: () => Promise + getIOSAppName: () => Promise } } diff --git a/src/extensions/workflows.ts b/src/extensions/workflows.ts index 6537161..5caedb2 100644 --- a/src/extensions/workflows.ts +++ b/src/extensions/workflows.ts @@ -1,4 +1,4 @@ -import { CycliToolbox } from '../types' +import { CycliToolbox, WorkflowEventType, WorkflowEvent } from '../types' import { basename } from 'path' module.exports = (toolbox: CycliToolbox) => { @@ -15,8 +15,28 @@ module.exports = (toolbox: CycliToolbox) => { return `${toolbox.projectConfig.getName()}-${workflowBasename}.yml` } + const eventsToWorkflowProps = ( + events: WorkflowEvent[] + ): Record => { + const props: Record = {} + + events.forEach((event: WorkflowEvent) => { + switch (event.type) { + case WorkflowEventType.PUSH: + props.push = event.branch ?? '' + break + case WorkflowEventType.PULL_REQUEST: + props.pull_request = 'true' + break + } + }) + + return props + } + const generate = async ( template: string, + { events }: { events: WorkflowEvent[] }, props: Record = {} ): Promise => { const pathRelativeToRoot = toolbox.context.path.relFromRepoRoot( @@ -48,6 +68,7 @@ module.exports = (toolbox: CycliToolbox) => { nodeVersionFile, bunVersionFile, pathRelativeToRoot, + ...eventsToWorkflowProps(events), ...props, }, }) @@ -79,6 +100,7 @@ export interface WorkflowsExtension { workflows: { generate: ( template: string, + { events }: { events: WorkflowEvent[] }, props?: Record ) => Promise } diff --git a/src/recipes/build.ts b/src/recipes/build.ts index 245b63d..dba0e4f 100644 --- a/src/recipes/build.ts +++ b/src/recipes/build.ts @@ -1,4 +1,4 @@ -import { CycliError, CycliToolbox, Platform } from '../types' +import { CycliToolbox, Platform, WorkflowEvent } from '../types' import { join } from 'path' const BuildMode = { @@ -8,11 +8,10 @@ const BuildMode = { type BuildModeType = (typeof BuildMode)[keyof typeof BuildMode] -const createBuildWorkflowForAndroid = async ( +const configureProjectForAndroidBuild = async ( toolbox: CycliToolbox, - { mode, expo }: { mode: BuildModeType; expo: boolean }, - workflowProps: Record = {} -): Promise => { + { mode }: { mode: BuildModeType } +): Promise => { const gradleCommands = () => { switch (mode) { case BuildMode.Debug: @@ -29,14 +28,23 @@ const createBuildWorkflowForAndroid = async ( '-Dorg.gradle.jvmargs=-Xmx4g', ].join(' ') - if (expo) { + if (toolbox.projectConfig.isExpo()) { script = `npx expo prebuild --${toolbox.context.packageManager()} && ${script}` } await toolbox.scripts.add(`build:${mode}:android`, script) + toolbox.interactive.success(`Configured project for Android ${mode} build.`) +} + +const generateBuildWorkflowForAndroid = async ( + toolbox: CycliToolbox, + { mode, events }: { mode: BuildModeType; events: WorkflowEvent[] }, + workflowProps: Record = {} +): Promise => { const workflowFileName = await toolbox.workflows.generate( join(`build-${mode}`, `build-${mode}-android.ejf`), + { events }, workflowProps ) @@ -45,19 +53,12 @@ const createBuildWorkflowForAndroid = async ( return workflowFileName } -const createBuildWorkflowForIOS = async ( +const configureProjectForIOSBuild = async ( toolbox: CycliToolbox, - { - mode, - iOSAppName, - expo, - }: { - mode: BuildModeType - iOSAppName: string - expo: boolean - }, - workflowProps: Record = {} -): Promise => { + { mode }: { mode: BuildModeType } +): Promise => { + const iOSAppName = await toolbox.projectConfig.getIOSAppName() + const configuration = () => { switch (mode) { case BuildMode.Debug: @@ -78,7 +79,7 @@ const createBuildWorkflowForIOS = async ( '-quiet', ].join(' ') - if (expo) { + if (toolbox.projectConfig.isExpo()) { script = `npx expo prebuild --${toolbox.context.packageManager()} && ${script}` } else { script = `cd ios && pod install && cd .. && ${script}` @@ -86,8 +87,19 @@ const createBuildWorkflowForIOS = async ( await toolbox.scripts.add(`build:${mode}:ios`, script) + toolbox.interactive.success(`Configured project for iOS ${mode} build.`) +} + +const generateBuildWorkflowForIOS = async ( + toolbox: CycliToolbox, + { mode, events }: { mode: BuildModeType; events: WorkflowEvent[] }, + workflowProps: Record = {} +): Promise => { + const iOSAppName = await toolbox.projectConfig.getIOSAppName() + const workflowFileName = await toolbox.workflows.generate( join(`build-${mode}`, `build-${mode}-ios.ejf`), + { events }, { iOSAppName, ...workflowProps, @@ -99,36 +111,18 @@ const createBuildWorkflowForIOS = async ( return workflowFileName } -export const createBuildWorkflows = async ( +export const configureProjectForBuild = async ( toolbox: CycliToolbox, - { mode, expo }: { mode: BuildModeType; expo: boolean } -): Promise<{ [key in Platform]: string }> => { - const existsAndroidDir = toolbox.filesystem.exists('android') - const existsIOsDir = toolbox.filesystem.exists('ios') - - if (expo) { + { mode }: { mode: BuildModeType } +): Promise => { + if (toolbox.projectConfig.isExpo()) { await toolbox.projectConfig.checkAppNameInConfigOrGenerate() - await toolbox.expo.prebuild({ cleanAfter: false }) } - const iOSAppName = toolbox.filesystem - .list('ios') - ?.find((file) => file.endsWith('.xcodeproj')) - ?.replace('.xcodeproj', '') - - if (!iOSAppName) { - throw CycliError( - 'Failed to obtain iOS app name. Perhaps your ios/ directory is missing *.xcodeproj file.' - ) - } - - let lookupDebugBuildWorkflowFileName = '' + await configureProjectForAndroidBuild(toolbox, { mode }) + await configureProjectForIOSBuild(toolbox, { mode }) if (mode === BuildMode.Debug) { - lookupDebugBuildWorkflowFileName = await toolbox.workflows.generate( - join('build-debug', 'lookup-cached-debug-build.ejf') - ) - await toolbox.scripts.add( 'fingerprint:android', "npx expo-updates fingerprint:generate --platform android | jq -r '.hash' | xargs -n 1 echo 'fingerprint:'" @@ -139,26 +133,33 @@ export const createBuildWorkflows = async ( "npx expo-updates fingerprint:generate --platform ios | jq -r '.hash' | xargs -n 1 echo 'fingerprint:'" ) } +} - const androidBuildWorkflowFileName = await createBuildWorkflowForAndroid( +export const generateBuildWorkflows = async ( + toolbox: CycliToolbox, + { mode, events }: { mode: BuildModeType; events: WorkflowEvent[] } +): Promise<{ [key in Platform]: string }> => { + let lookupDebugBuildWorkflowFileName = '' + + if (mode === BuildMode.Debug) { + lookupDebugBuildWorkflowFileName = await toolbox.workflows.generate( + join('build-debug', 'lookup-cached-debug-build.ejf'), + { events: [] } + ) + } + + const androidBuildWorkflowFileName = await generateBuildWorkflowForAndroid( toolbox, - { mode, expo }, + { mode, events }, { lookupDebugBuildWorkflowFileName } ) - const iOSBuildWorkflowFileName = await createBuildWorkflowForIOS( + const iOSBuildWorkflowFileName = await generateBuildWorkflowForIOS( toolbox, - { - mode, - iOSAppName, - expo, - }, + { mode, events }, { lookupDebugBuildWorkflowFileName } ) - if (!existsAndroidDir) toolbox.filesystem.remove('android') - if (!existsIOsDir) toolbox.filesystem.remove('ios') - return { android: androidBuildWorkflowFileName, ios: iOSBuildWorkflowFileName, diff --git a/src/recipes/detox.ts b/src/recipes/detox.ts index 932ef53..d6ee957 100644 --- a/src/recipes/detox.ts +++ b/src/recipes/detox.ts @@ -1,6 +1,12 @@ -import { CycliRecipe, CycliRecipeType, CycliToolbox } from '../types' -import { createBuildWorkflows } from './build' +import { + CycliRecipe, + CycliRecipeType, + CycliToolbox, + WorkflowEvent, + WorkflowEventType, +} from '../types' import { join } from 'path' +import { configureProjectForBuild, generateBuildWorkflows } from './build' const DETOX_BARE_PROJECT_CONFIG_URL = `https://wix.github.io/Detox/docs/next/introduction/project-setup/#step-4-additional-android-configuration` const DETOX_EXPO_PLUGIN = '@config-plugins/detox' @@ -24,9 +30,9 @@ const addDetoxExpoPlugin = async (toolbox: CycliToolbox) => { } } -const execute = async (toolbox: CycliToolbox) => { +const configureProject = async (toolbox: CycliToolbox) => { toolbox.interactive.vspace() - toolbox.interactive.sectionHeader('Generating Detox workflow') + toolbox.interactive.sectionHeader('Configuring project for Detox') const expo = toolbox.projectConfig.isExpo() @@ -45,10 +51,7 @@ const execute = async (toolbox: CycliToolbox) => { ) } - await createBuildWorkflows(toolbox, { - mode: 'release', - expo, - }) + await configureProjectForBuild(toolbox, { mode: 'release' }) await toolbox.dependencies.addDev('detox') // >=29 because of https://wix.github.io/Detox/docs/introduction/project-setup#step-1-bootstrap @@ -116,11 +119,27 @@ const execute = async (toolbox: CycliToolbox) => { toolbox.furtherActions.push(starterTestMessage) } - await toolbox.workflows.generate(join('detox', 'test-detox-android.ejf')) + toolbox.interactive.success('Configured project for Detox.') +} + +const generateWorkflow = async ( + toolbox: CycliToolbox, + events: WorkflowEvent[] +) => { + await generateBuildWorkflows(toolbox, { mode: 'release', events }) - await toolbox.workflows.generate(join('detox', 'test-detox-ios.ejf')) + await toolbox.workflows.generate(join('detox', 'test-detox-android.ejf'), { + events, + }) + await toolbox.workflows.generate(join('detox', 'test-detox-ios.ejf'), { + events, + }) - toolbox.interactive.success('Created Detox workflow.') + toolbox.interactive.success( + `Created Detox workflow for events: [${events + .map((e) => e.type) + .join(', ')}]` + ) } export const recipe: CycliRecipe = { @@ -129,8 +148,10 @@ export const recipe: CycliRecipe = { flag: CycliRecipeType.DETOX, description: 'Generate workflow to run Detox e2e tests on every PR', selectHint: 'run detox e2e tests suite', + allowedEvents: [WorkflowEventType.PUSH, WorkflowEventType.PULL_REQUEST], }, - execute, + configureProject, + generateWorkflow, } as const export default recipe diff --git a/src/recipes/eas.ts b/src/recipes/eas.ts index 5ddf11a..73fcce3 100644 --- a/src/recipes/eas.ts +++ b/src/recipes/eas.ts @@ -4,6 +4,8 @@ import { CycliRecipe, CycliRecipeType, CycliToolbox, + WorkflowEvent, + WorkflowEventType, } from '../types' import { join } from 'path' import { recursiveAssign } from '../utils/recursiveAssign' @@ -50,9 +52,9 @@ const patchAppJson = async (toolbox: CycliToolbox): Promise => { await toolbox.projectConfig.patchAppConfig(patch) } -const execute = async (toolbox: CycliToolbox): Promise => { +const configureProject = async (toolbox: CycliToolbox): Promise => { toolbox.interactive.vspace() - toolbox.interactive.sectionHeader('Generating Preview with EAS workflow') + toolbox.interactive.sectionHeader('Configuring project for Preview with EAS') await toolbox.dependencies.add('expo') await toolbox.dependencies.add('expo-dev-client') @@ -91,9 +93,7 @@ const execute = async (toolbox: CycliToolbox): Promise => { await patchEasJson(toolbox, withIOSCredentials) await patchAppJson(toolbox) - await toolbox.workflows.generate(join('eas', 'eas.ejf')) - - toolbox.interactive.success('Created Preview with EAS workflow.') + toolbox.interactive.success('Configured project for Preview with EAS') toolbox.interactive.warning( `Remember to create repository secret EXPO_TOKEN for Preview with EAS workflow to work properly. For more information check ${REPOSITORY_SECRETS_HELP_URL}` @@ -103,6 +103,19 @@ const execute = async (toolbox: CycliToolbox): Promise => { ) } +const generateWorkflow = async ( + toolbox: CycliToolbox, + events: WorkflowEvent[] +): Promise => { + await toolbox.workflows.generate(join('eas', 'eas.ejf'), { events }) + + toolbox.interactive.success( + `Created Preview with EAS workflow for events: [${events + .map((e) => e.type) + .join(', ')}]` + ) +} + const validate = (toolbox: CycliToolbox): void => { if (!toolbox.projectConfig.isExpo()) { throw CycliError('only supported in expo projects') @@ -116,8 +129,10 @@ export const recipe: CycliRecipe = { description: 'Generate Preview with EAS workflow to run on every PR (Expo projects only)', selectHint: 'generate preview with EAS', + allowedEvents: [WorkflowEventType.PULL_REQUEST], }, - execute, + configureProject, + generateWorkflow, validate, } diff --git a/src/recipes/jest.ts b/src/recipes/jest.ts index 18415a3..5312394 100644 --- a/src/recipes/jest.ts +++ b/src/recipes/jest.ts @@ -1,13 +1,19 @@ import { join } from 'path' -import { CycliRecipe, CycliRecipeType, CycliToolbox } from '../types' +import { + CycliRecipe, + CycliRecipeType, + CycliToolbox, + WorkflowEvent, + WorkflowEventType, +} from '../types' const existsJestConfiguration = (toolbox: CycliToolbox): boolean => Boolean(toolbox.projectConfig.packageJson().jest) || Boolean(toolbox.filesystem.list()?.some((f) => f.startsWith('jest.config.'))) -const execute = async (toolbox: CycliToolbox): Promise => { +const configureProject = async (toolbox: CycliToolbox): Promise => { toolbox.interactive.vspace() - toolbox.interactive.sectionHeader('Generating Jest workflow') + toolbox.interactive.sectionHeader('Configuring project for Jest') await toolbox.dependencies.addDev('jest') @@ -24,9 +30,20 @@ const execute = async (toolbox: CycliToolbox): Promise => { ) } - await toolbox.workflows.generate(join('jest', 'jest.ejf')) + toolbox.interactive.success('Configured project for Jest.') +} - toolbox.interactive.success('Created Jest workflow.') +const generateWorkflow = async ( + toolbox: CycliToolbox, + events: WorkflowEvent[] +): Promise => { + await toolbox.workflows.generate(join('jest', 'jest.ejf'), { events }) + + toolbox.interactive.success( + `Created Jest workflow for events: [${events + .map((e) => e.type) + .join(', ')}]` + ) } export const recipe: CycliRecipe = { @@ -35,8 +52,10 @@ export const recipe: CycliRecipe = { flag: CycliRecipeType.JEST, description: 'Generate Jest workflow to run on every PR', selectHint: 'run tests with Jest', + allowedEvents: [WorkflowEventType.PUSH, WorkflowEventType.PULL_REQUEST], }, - execute, + configureProject, + generateWorkflow, } export default recipe diff --git a/src/recipes/lint.ts b/src/recipes/lint.ts index 360177d..dc1ba91 100644 --- a/src/recipes/lint.ts +++ b/src/recipes/lint.ts @@ -1,4 +1,10 @@ -import { CycliRecipe, CycliRecipeType, CycliToolbox } from '../types' +import { + CycliRecipe, + CycliRecipeType, + CycliToolbox, + WorkflowEvent, + WorkflowEventType, +} from '../types' import { join } from 'path' const existsEslintConfiguration = (toolbox: CycliToolbox): boolean => @@ -42,9 +48,9 @@ const generateConfigForExpo = async (toolbox: CycliToolbox): Promise => { ) } -const execute = async (toolbox: CycliToolbox): Promise => { +const configureProject = async (toolbox: CycliToolbox): Promise => { toolbox.interactive.vspace() - toolbox.interactive.sectionHeader('Generating ESLint workflow') + toolbox.interactive.sectionHeader('Configuring project for ESLint') // eslint@9 introduces new configuration format that is not supported by widely used plugins yet, // so we stick to ^8 for now. @@ -52,7 +58,7 @@ const execute = async (toolbox: CycliToolbox): Promise => { await toolbox.dependencies.addDev('typescript') const withPrettier = - toolbox.config.getSelectedRecipes().includes(CycliRecipeType.PRETTIER) || + (await toolbox.config.getSelectedRecipes()).has(CycliRecipeType.PRETTIER) || toolbox.dependencies.existsDev('prettier') || toolbox.dependencies.exists('prettier') @@ -80,9 +86,20 @@ const execute = async (toolbox: CycliToolbox): Promise => { await toolbox.scripts.add('lint', "eslint '**/*.{js,jsx,ts,tsx}'") - await toolbox.workflows.generate(join('lint', 'lint.ejf')) + toolbox.interactive.success('Configured project for ESLint.') +} - toolbox.interactive.success('Created ESLint workflow.') +const generateWorkflow = async ( + toolbox: CycliToolbox, + events: WorkflowEvent[] +): Promise => { + await toolbox.workflows.generate(join('lint', 'lint.ejf'), { events }) + + toolbox.interactive.success( + `Created ESLint workflow for events: [${events + .map((e) => e.type) + .join(', ')}]` + ) } export const recipe: CycliRecipe = { @@ -91,8 +108,10 @@ export const recipe: CycliRecipe = { flag: CycliRecipeType.ESLINT, description: 'Generate ESLint workflow to run on every PR', selectHint: 'check code style with linter', + allowedEvents: [WorkflowEventType.PUSH, WorkflowEventType.PULL_REQUEST], }, - execute, + configureProject, + generateWorkflow, } export default recipe diff --git a/src/recipes/maestro.ts b/src/recipes/maestro.ts index 1e0089a..3517d4e 100644 --- a/src/recipes/maestro.ts +++ b/src/recipes/maestro.ts @@ -1,20 +1,18 @@ -import { CycliRecipe, CycliRecipeType, CycliToolbox } from '../types' -import { createBuildWorkflows } from './build' +import { + CycliRecipe, + CycliRecipeType, + CycliToolbox, + WorkflowEvent, + WorkflowEventType, +} from '../types' +import { configureProjectForBuild, generateBuildWorkflows } from './build' import { join } from 'path' -const execute = async (toolbox: CycliToolbox) => { +const configureProject = async (toolbox: CycliToolbox) => { toolbox.interactive.vspace() - toolbox.interactive.sectionHeader('Genereating Maestro workflow') + toolbox.interactive.sectionHeader('Configuring project for Maestro') - const expo = toolbox.projectConfig.isExpo() - - const { - android: androidDebugBuildWorkflowFileName, - ios: iOSDebugBuildWorkflowFileName, - } = await createBuildWorkflows(toolbox, { - mode: 'debug', - expo, - }) + await configureProjectForBuild(toolbox, { mode: 'debug' }) await toolbox.scripts.add( 'maestro:test', @@ -41,22 +39,43 @@ const execute = async (toolbox: CycliToolbox) => { const exampleFlowMessage = 'Remember to edit .maestro/example-flow.yml to match your app.' + toolbox.interactive.success('Configured project for Maestro.') + toolbox.interactive.warning(exampleFlowMessage) toolbox.furtherActions.push(exampleFlowMessage) } +} + +const generateWorkflow = async ( + toolbox: CycliToolbox, + events: WorkflowEvent[] +) => { + const { + android: androidDebugBuildWorkflowFileName, + ios: iOSDebugBuildWorkflowFileName, + } = await generateBuildWorkflows(toolbox, { mode: 'debug', events }) await toolbox.workflows.generate( join('maestro', 'maestro-test-android.ejf'), + { events }, { androidDebugBuildWorkflowFileName, } ) - await toolbox.workflows.generate(join('maestro', 'maestro-test-ios.ejf'), { - iOSDebugBuildWorkflowFileName, - }) + await toolbox.workflows.generate( + join('maestro', 'maestro-test-ios.ejf'), + { events }, + { + iOSDebugBuildWorkflowFileName, + } + ) - toolbox.interactive.success('Created Maestro workflow.') + toolbox.interactive.success( + `Created Maestro workflow for events: [${events + .map((e) => e.type) + .join(', ')}]` + ) } export const recipe: CycliRecipe = { @@ -65,8 +84,10 @@ export const recipe: CycliRecipe = { flag: CycliRecipeType.MAESTRO, description: 'Generate workflow to run Maestro e2e tests on every PR', selectHint: 'run maestro e2e tests suite', + allowedEvents: [WorkflowEventType.PUSH, WorkflowEventType.PULL_REQUEST], }, - execute, + configureProject, + generateWorkflow, } as const export default recipe diff --git a/src/recipes/prettier.ts b/src/recipes/prettier.ts index 218ac33..82693b6 100644 --- a/src/recipes/prettier.ts +++ b/src/recipes/prettier.ts @@ -1,4 +1,10 @@ -import { CycliRecipe, CycliRecipeType, CycliToolbox } from '../types' +import { + CycliRecipe, + CycliRecipeType, + CycliToolbox, + WorkflowEvent, + WorkflowEventType, +} from '../types' import { join } from 'path' const existsPrettierConfiguration = (toolbox: CycliToolbox): boolean => @@ -11,9 +17,9 @@ const existsPrettierConfiguration = (toolbox: CycliToolbox): boolean => ) ) -const execute = async (toolbox: CycliToolbox): Promise => { +const configureProject = async (toolbox: CycliToolbox): Promise => { toolbox.interactive.vspace() - toolbox.interactive.sectionHeader('Generating Prettier check workflow') + toolbox.interactive.sectionHeader('Configuring project for Prettier check') await toolbox.dependencies.addDev('prettier') @@ -27,8 +33,6 @@ const execute = async (toolbox: CycliToolbox): Promise => { 'prettier --write "**/*.{ts,tsx,js,jsx,json,css,scss,md}"' ) - await toolbox.workflows.generate(join('prettier', 'prettier.ejf')) - if (!existsPrettierConfiguration(toolbox)) { await toolbox.template.generate({ template: join('prettier', '.prettierrc.ejs'), @@ -45,7 +49,20 @@ const execute = async (toolbox: CycliToolbox): Promise => { toolbox.interactive.step('Created default .prettierignore file.') } - toolbox.interactive.success('Created Prettier check workflow.') + toolbox.interactive.success('Configured project for Prettier check.') +} + +const generateWorkflow = async ( + toolbox: CycliToolbox, + events: WorkflowEvent[] +): Promise => { + await toolbox.workflows.generate(join('prettier', 'prettier.ejf'), { events }) + + toolbox.interactive.success( + `Created Prettier check workflow for events: [${events + .map((e) => e.type) + .join(', ')}]` + ) } export const recipe: CycliRecipe = { @@ -54,8 +71,10 @@ export const recipe: CycliRecipe = { flag: CycliRecipeType.PRETTIER, description: 'Generate Prettier check workflow to run on every PR', selectHint: 'check code format with prettier', + allowedEvents: [WorkflowEventType.PUSH, WorkflowEventType.PULL_REQUEST], }, - execute, + configureProject, + generateWorkflow, } export default recipe diff --git a/src/recipes/typescript.ts b/src/recipes/typescript.ts index 2bb37d6..f5bb139 100644 --- a/src/recipes/typescript.ts +++ b/src/recipes/typescript.ts @@ -1,7 +1,13 @@ -import { CycliRecipe, CycliRecipeType, CycliToolbox } from '../types' +import { + CycliRecipe, + CycliRecipeType, + CycliToolbox, + WorkflowEvent, + WorkflowEventType, +} from '../types' import { join } from 'path' -const execute = async (toolbox: CycliToolbox): Promise => { +const configureProject = async (toolbox: CycliToolbox): Promise => { toolbox.interactive.vspace() toolbox.interactive.sectionHeader('Generating Typescript check workflow') @@ -9,8 +15,6 @@ const execute = async (toolbox: CycliToolbox): Promise => { await toolbox.scripts.add('ts:check', 'tsc -p . --noEmit') - await toolbox.workflows.generate(join('typescript', 'typescript.ejf')) - if (!toolbox.filesystem.exists('tsconfig.json')) { await toolbox.template.generate({ template: join('typescript', 'tsconfig.json.ejs'), @@ -22,7 +26,22 @@ const execute = async (toolbox: CycliToolbox): Promise => { ) } - toolbox.interactive.success('Created Typescript check workflow.') + toolbox.interactive.success('Configured project for Typescript check.') +} + +const generateWorkflow = async ( + toolbox: CycliToolbox, + events: WorkflowEvent[] +): Promise => { + await toolbox.workflows.generate(join('typescript', 'typescript.ejf'), { + events, + }) + + toolbox.interactive.success( + `Created Typescript Check workflow for events: [${events + .map((e) => e.type) + .join(', ')}]` + ) } export const recipe: CycliRecipe = { @@ -31,8 +50,10 @@ export const recipe: CycliRecipe = { flag: CycliRecipeType.TYPESCRIPT, description: 'Generate Typescript check workflow to run on every PR', selectHint: 'run typescript check to find compilation errors', + allowedEvents: [WorkflowEventType.PUSH, WorkflowEventType.PULL_REQUEST], }, - execute, + configureProject, + generateWorkflow, } export default recipe diff --git a/src/templates/build-release/build-release-android.ejf b/src/templates/build-release/build-release-android.ejf index cd1ef82..f70cecc 100644 --- a/src/templates/build-release/build-release-android.ejf +++ b/src/templates/build-release/build-release-android.ejf @@ -1,9 +1,6 @@ name: Build Android Release App -on: - pull_request: <%= (props.isMonorepo) ? ` - paths: - - ${props.pathRelativeToRoot}/**` : "" %> +<%- include('../common/triggerEvents', { props }); %> jobs: lookup-cached-build: diff --git a/src/templates/build-release/build-release-ios.ejf b/src/templates/build-release/build-release-ios.ejf index 9d2ba0c..204563e 100644 --- a/src/templates/build-release/build-release-ios.ejf +++ b/src/templates/build-release/build-release-ios.ejf @@ -1,9 +1,6 @@ name: Build iOS Release App -on: - pull_request: <%= (props.isMonorepo) ? ` - paths: - - ${props.pathRelativeToRoot}/**` : "" %> +<%- include('../common/triggerEvents', { props }); %> jobs: lookup-cached-build: diff --git a/src/templates/common/triggerEvents.ejs b/src/templates/common/triggerEvents.ejs new file mode 100644 index 0000000..d515b32 --- /dev/null +++ b/src/templates/common/triggerEvents.ejs @@ -0,0 +1,13 @@ +on: +<%= (props.pull_request) ? ` + pull_request: ${(props.isMonorepo) ? ` + paths: + - ${props.pathRelativeToRoot}/**` : "" + }` : "" %> <%= (props.push) ? ` + push: + branches: + - ${props.push} ${(props.isMonorepo) ? ` + paths: + - ${props.pathRelativeToRoot}/**` : "" + }` : "" +%> diff --git a/src/templates/detox/test-detox-android.ejf b/src/templates/detox/test-detox-android.ejf index 4d6e8e2..cae36f9 100644 --- a/src/templates/detox/test-detox-android.ejf +++ b/src/templates/detox/test-detox-android.ejf @@ -1,9 +1,6 @@ name: Test Detox Android -on: - pull_request: <%= (props.isMonorepo) ? ` - paths: - - ${props.pathRelativeToRoot}/**` : "" %> +<%- include('../common/triggerEvents', { props }); %> jobs: test-detox-android: diff --git a/src/templates/detox/test-detox-ios.ejf b/src/templates/detox/test-detox-ios.ejf index bf5d221..3c38b6e 100644 --- a/src/templates/detox/test-detox-ios.ejf +++ b/src/templates/detox/test-detox-ios.ejf @@ -1,9 +1,6 @@ name: Test Detox iOS -on: - pull_request: <%= (props.isMonorepo) ? ` - paths: - - ${props.pathRelativeToRoot}/**` : "" %> +<%- include('../common/triggerEvents', { props }); %> jobs: test-detox-ios: diff --git a/src/templates/eas/eas.ejf b/src/templates/eas/eas.ejf index ed12f55..db00cc2 100644 --- a/src/templates/eas/eas.ejf +++ b/src/templates/eas/eas.ejf @@ -1,9 +1,6 @@ name: Preview with EAS -on: -pull_request: <%= (props.isMonorepo) ? ` - paths: - - ${props.pathRelativeToRoot}/**` : "" %> +<%- include('../common/triggerEvents', { props }); %> jobs: preview-with-eas: diff --git a/src/templates/jest/jest.ejf b/src/templates/jest/jest.ejf index 4916a48..2009f45 100644 --- a/src/templates/jest/jest.ejf +++ b/src/templates/jest/jest.ejf @@ -1,9 +1,6 @@ name: Run Jest tests -on: - pull_request: <%= (props.isMonorepo) ? ` - paths: - - ${props.pathRelativeToRoot}/**` : "" %> +<%- include('../common/triggerEvents', { props }); %> jobs: jest: diff --git a/src/templates/lint/lint.ejf b/src/templates/lint/lint.ejf index 224a701..427f6f7 100644 --- a/src/templates/lint/lint.ejf +++ b/src/templates/lint/lint.ejf @@ -1,10 +1,7 @@ name: Check ESLint -on: - pull_request: <%= (props.isMonorepo) ? ` - paths: - - ${props.pathRelativeToRoot}/**` : "" %> - +<%- include('../common/triggerEvents', { props }); %> + jobs: eslint: name: ESLint diff --git a/src/templates/maestro/maestro-test-android.ejf b/src/templates/maestro/maestro-test-android.ejf index 2f7b6f3..51712c7 100644 --- a/src/templates/maestro/maestro-test-android.ejf +++ b/src/templates/maestro/maestro-test-android.ejf @@ -8,10 +8,7 @@ const startBundler = { name: Maestro Test Android -on: - pull_request: <%= (props.isMonorepo) ? ` - paths: - - ${props.pathRelativeToRoot}/**` : "" %> +<%- include('../common/triggerEvents', { props }); %> jobs: build-debug-android: diff --git a/src/templates/maestro/maestro-test-ios.ejf b/src/templates/maestro/maestro-test-ios.ejf index 42ec235..fc2e0cf 100644 --- a/src/templates/maestro/maestro-test-ios.ejf +++ b/src/templates/maestro/maestro-test-ios.ejf @@ -8,10 +8,7 @@ const startBundler = { name: Maestro Test iOS -on: - pull_request: <%= (props.isMonorepo) ? ` - paths: - - ${props.pathRelativeToRoot}/**` : "" %> +<%- include('../common/triggerEvents', { props }); %> jobs: build-debug-ios: diff --git a/src/templates/prettier/prettier.ejf b/src/templates/prettier/prettier.ejf index 3d9c146..66b5fbe 100644 --- a/src/templates/prettier/prettier.ejf +++ b/src/templates/prettier/prettier.ejf @@ -1,9 +1,6 @@ name: Prettier check -on: - pull_request: <%= (props.isMonorepo) ? ` - paths: - - ${props.pathRelativeToRoot}/**` : "" %> +<%- include('../common/triggerEvents', { props }); %> jobs: prettier-check: diff --git a/src/templates/typescript/typescript.ejf b/src/templates/typescript/typescript.ejf index 4c2851f..3b30dfe 100644 --- a/src/templates/typescript/typescript.ejf +++ b/src/templates/typescript/typescript.ejf @@ -1,9 +1,6 @@ name: Run Typescript -on: - pull_request: <%= (props.isMonorepo) ? ` - paths: - - ${props.pathRelativeToRoot}/**` : "" %> +<%- include('../common/triggerEvents', { props }); %> jobs: typescript-check: diff --git a/src/types.ts b/src/types.ts index c75dc36..c3e3423 100644 --- a/src/types.ts +++ b/src/types.ts @@ -13,6 +13,7 @@ import { ExpoExtension } from './extensions/expo' import { PrettierExtension } from './extensions/prettier' import { TelemetryExtension } from './extensions/telemetry' import { ConfigExtension } from './extensions/config' +import { ExecutorExtension } from './extensions/executor' export enum CycliRecipeType { ESLINT = 'lint', @@ -24,16 +25,33 @@ export enum CycliRecipeType { EAS = 'eas', } +export enum WorkflowEventType { + PULL_REQUEST = 'pull_request', + PUSH = 'push', +} + +export type WorkflowEvent = { + type: WorkflowEventType + branch?: string +} + +export type CycliConfig = Map> + export interface RecipeMeta { name: string flag: CycliRecipeType description: string selectHint: string + allowedEvents: WorkflowEventType[] } export interface CycliRecipe { meta: RecipeMeta - execute: (toolbox: CycliToolbox) => Promise + configureProject: (toolbox: CycliToolbox) => Promise + generateWorkflow: ( + toolbox: CycliToolbox, + events: WorkflowEvent[] + ) => Promise validate?: (toolbox: CycliToolbox) => void } @@ -104,4 +122,5 @@ export type CycliToolbox = { FurtherActionsExtension & ExpoExtension & PrettierExtension & - TelemetryExtension + TelemetryExtension & + ExecutorExtension diff --git a/src/utils/sequentialPromiseMap.ts b/src/utils/sequentialPromiseMap.ts deleted file mode 100644 index ff03bac..0000000 --- a/src/utils/sequentialPromiseMap.ts +++ /dev/null @@ -1,13 +0,0 @@ -const sequentialPromiseMap = async ( - input: T[], - mapper: (arg: T) => Promise -): Promise => { - const results: S[] = [] - for (const next of input) { - const nextResult = await mapper(next) - if (nextResult) results.push(nextResult) - } - return results -} - -export default sequentialPromiseMap From 977a83354b33608ea43efb71fcc8018b15f4bd06 Mon Sep 17 00:00:00 2001 From: km1chno Date: Thu, 19 Dec 2024 15:01:56 +0100 Subject: [PATCH 2/3] fix typescript message --- src/recipes/typescript.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/recipes/typescript.ts b/src/recipes/typescript.ts index f5bb139..5d1bc64 100644 --- a/src/recipes/typescript.ts +++ b/src/recipes/typescript.ts @@ -9,7 +9,7 @@ import { join } from 'path' const configureProject = async (toolbox: CycliToolbox): Promise => { toolbox.interactive.vspace() - toolbox.interactive.sectionHeader('Generating Typescript check workflow') + toolbox.interactive.sectionHeader('Configuring project for Typescript check') await toolbox.dependencies.addDev('typescript') @@ -38,7 +38,7 @@ const generateWorkflow = async ( }) toolbox.interactive.success( - `Created Typescript Check workflow for events: [${events + `Created Typescript check workflow for events: [${events .map((e) => e.type) .join(', ')}]` ) From efab17dbb11065faa48e1751880cbf614ba5535c Mon Sep 17 00:00:00 2001 From: km1chno Date: Thu, 19 Dec 2024 15:21:06 +0100 Subject: [PATCH 3/3] fix help message test --- __tests__/cli.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/__tests__/cli.test.ts b/__tests__/cli.test.ts index 62c7816..9affab3 100644 --- a/__tests__/cli.test.ts +++ b/__tests__/cli.test.ts @@ -19,7 +19,7 @@ test('prints help', async () => { '--skip-git-check', '-pull-request [...workflows]', '-main [...workflows]', - 'Use any combination of the following with --pull-request and --main flags to specify your own set of workflows to generate', + 'Use any combination of the following with -pull-request and -main flags to specify your own set of workflows to generate', 'lint', 'jest', 'ts',