Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add detox recipe #22

Merged
merged 32 commits into from
Aug 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
5d53923
Start progress on detox
maciekstosio Jul 5, 2024
0b90286
Merge branch 'main' into @maciekstosio/feat-add-detox
maciekstosio Jul 12, 2024
9fbf36c
chore: move detox
maciekstosio Jul 12, 2024
78b30c5
working android
maciekstosio Jul 12, 2024
b7be496
Handle skip git check properly
maciekstosio Jul 16, 2024
39f9092
Handle separate build and detox (now working yet)
maciekstosio Jul 16, 2024
cb6ba38
Separate android and build and use cache
maciekstosio Jul 17, 2024
e15115f
rename key
maciekstosio Jul 17, 2024
0d873cc
Merge branch 'main' into @maciekstosio/feat-add-detox
km1chno Jul 17, 2024
6f16c2c
add missing eol in templates
km1chno Jul 17, 2024
17e15d6
fix android emulator not finishing
km1chno Jul 22, 2024
cbba5b5
add ios detox recipe and ios release build
km1chno Jul 23, 2024
4530df0
move skip git check flag to main file
km1chno Jul 23, 2024
87f8611
Merge branch 'main' into @maciekstosio/feat-add-detox
km1chno Jul 23, 2024
e477047
rectructurize build workflows
km1chno Jul 23, 2024
ded65d7
support monorepos and npm
km1chno Jul 23, 2024
5769ba6
use wait-for-status-checks action instead of needs
km1chno Jul 25, 2024
85b87fc
test on release builds
km1chno Jul 29, 2024
8be1db1
Merge branch 'main' into @maciekstosio/feat-add-detox
km1chno Jul 29, 2024
295b167
fix github head sha
km1chno Jul 29, 2024
2158fc5
add info about github token to readme
km1chno Jul 29, 2024
3f30bec
add warning about creating GH_TOKEN after completing detox recipe
km1chno Jul 29, 2024
c79403b
change test apk path
km1chno Jul 30, 2024
42ae72e
Merge branch 'main' into @maciekstosio/feat-add-detox
km1chno Aug 7, 2024
834a452
Merge branch 'main' into @maciekstosio/feat-add-detox
km1chno Aug 7, 2024
3af97de
add appJson to projectConfig extension
km1chno Aug 7, 2024
6daad41
change expo prebuild message
km1chno Aug 7, 2024
3d83c0d
add skipInstalledCheck flag to dependencies.add
km1chno Aug 8, 2024
123e24f
fix detox and release build recipes in monorepo
km1chno Aug 8, 2024
38fa2a3
Change detox non-expo error to not terminate the whole script
km1chno Aug 8, 2024
16071f6
change version and skipInstalledCheck params to object
km1chno Aug 9, 2024
9b640a4
remove redundant empty lines in workflows yamls before saving
km1chno Aug 9, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ The following are **feature flags** that can be used in silent mode (they are ig
<td style="vertical-align: middle;">--eas-update</td>
<td style="vertical-align: middle;">Generate EAS Update and preview workflow to run on every PR.</td>
</tr>
<tr>
<td style="vertical-align: middle;">--detox</td>
<td style="vertical-align: middle;">Generate workflow to run Detox e2e tests on every PR.</td>
</tr>
</table>

## 🔐 Repository secrets
Expand All @@ -87,6 +91,14 @@ as you will always be prompted to create secrets if necessary. The following tab
<td style="vertical-align: middle;">EXPO_TOKEN</td>
<td style="vertical-align: middle;">Used for authentication in workflows using your Expo account. Learn more at <a href=https://docs.expo.dev/eas-update/github-actions>Expo with GitHub actions</a>.</td>
</tr>
<tr>
<td style="vertical-align: middle;">GH_TOKEN</td>
<td style="vertical-align: middle;">
Used by workflows reading status of other GitHub actions. You can learn how to generate the token at
<a href=https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens>Managing your personal access tokens</a>.
Make sure that your generated token has <ins>read access to actions</ins>.
</td>
</tr>
</table>

## 💬 Your feedback
Expand Down
4 changes: 4 additions & 0 deletions src/commands/react-native-ci-cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import jest from '../recipes/jest'
import typescriptCheck from '../recipes/typescript'
import prettierCheck from '../recipes/prettier'
import easUpdate from '../recipes/eas-update'
import detox from '../recipes/detox'
import isGitDirty from 'is-git-dirty'
import sequentialPromiseMap from '../utils/sequentialPromiseMap'
import { CycliToolbox, ProjectContext } from '../types'
Expand Down Expand Up @@ -46,13 +47,15 @@ const runReactNativeCiCli = async (toolbox: CycliToolbox) => {
const typescriptExecutor = await typescriptCheck.run(toolbox, context)
const prettierExecutor = await prettierCheck.run(toolbox, context)
const easUpdateExecutor = await easUpdate.run(toolbox, context)
const detoxExecutor = await detox.run(toolbox, context)

const executors = [
lintExecutor,
jestExecutor,
typescriptExecutor,
prettierExecutor,
easUpdateExecutor,
detoxExecutor,
].filter((executor) => executor != null)

if (executors.length === 0) {
Expand Down Expand Up @@ -88,6 +91,7 @@ const getFeatureOptions = (): Option[] => {
typescriptCheck.meta,
prettierCheck.meta,
easUpdate.meta,
detox.meta,
].map((meta) => ({
flag: meta.flag,
description: meta.description,
Expand Down
105 changes: 45 additions & 60 deletions src/extensions/dependencies.ts
Original file line number Diff line number Diff line change
@@ -1,54 +1,30 @@
import { CycliToolbox, ProjectContext } from '../types'

module.exports = (toolbox: CycliToolbox) => {
const { packageManager, semver } = toolbox
const { packageManager } = toolbox

const getSemver = (name: string): string | undefined => {
return toolbox.projectConfig.packageJson().dependencies?.[name]
}

const getSemverDev = (name: string): string | undefined => {
return toolbox.projectConfig.packageJson().devDependencies?.[name]
}

const exists = (name: string): boolean => !!getSemver(name)

const existsDev = (name: string): boolean => !!getSemverDev(name)

const isSatisfied = (name: string, version: string): boolean => {
const currentVersion = getSemver(name)
return (
!!currentVersion &&
(version === '' || semver.satisfies(currentVersion, version))
)
}
km1chno marked this conversation as resolved.
Show resolved Hide resolved
const exists = (name: string): boolean =>
Boolean(toolbox.projectConfig.packageJson().dependencies?.[name])

const isSatisfiedDev = (name: string, version: string): boolean => {
const currentVersion = getSemverDev(name)
return (
!!currentVersion &&
(version === '' || semver.satisfies(currentVersion, version))
)
}
const existsDev = (name: string): boolean =>
Boolean(toolbox.projectConfig.packageJson().devDependencies?.[name])

const add = async (name: string, context: ProjectContext, version = '') => {
if (existsDev(name)) {
toolbox.interactive.warning(
`Moving ${name} from "devDependencies" to "dependencies".`
)

const spinner = toolbox.interactive.spin(
`🗑️ Removing ${name} from "devDependencies"...`
)
await packageManager.remove(name, { dev: true })
spinner.stop()
const add = async (
name: string,
context: ProjectContext,
{
version = '',
skipInstalledCheck = false,
}: { version?: string; skipInstalledCheck?: boolean } = {
version: '',
skipInstalledCheck: false,
}

) => {
const fullName = version ? [name, version].join('@') : name

if (isSatisfied(name, version)) {
if (!skipInstalledCheck && exists(name)) {
toolbox.interactive.step(
`Dependency ${fullName} is already satisfied, skipping adding dependency.`
`Dependency ${name} is already installed, skipping adding dependency.`
)
return
}
Expand All @@ -68,39 +44,44 @@ module.exports = (toolbox: CycliToolbox) => {
const addDev = async (
name: string,
context: ProjectContext,
version = ''
{
version = '',
skipInstalledCheck = false,
}: { version?: string; skipInstalledCheck?: boolean } = {
version: '',
skipInstalledCheck: false,
}
) => {
if (exists(name)) {
toolbox.interactive.warning(
`Detected package ${name} in "dependencies", but shouldn't it be in "devDependencies"?`
)
add(name, context, version)
add(name, context, { version, skipInstalledCheck })
return
}

const fullName = version ? [name, version].join('@') : name

if (isSatisfiedDev(name, version)) {
if (!skipInstalledCheck && existsDev(name)) {
toolbox.interactive.step(
`Dev dependency ${fullName} is already satisfied, skipping adding dependency.`
)
} else {
const spinner = toolbox.interactive.spin(
`📦 Installing ${fullName} as devDependency...`
`Dev dependency ${name} is already installed, skipping adding dependency.`
)
await packageManager.add(fullName, {
dev: true,
force: context.packageManager,
})
spinner.stop()
return
}

const spinner = toolbox.interactive.spin(
`📦 Installing ${fullName} as devDependency...`
)
await packageManager.add(fullName, {
dev: true,
force: context.packageManager,
})
spinner.stop()

toolbox.interactive.step(`Installed ${fullName} as devDependency.`)
}

toolbox.dependencies = {
isSatisfied,
isSatisfiedDev,
exists,
existsDev,
add,
Expand All @@ -110,19 +91,23 @@ module.exports = (toolbox: CycliToolbox) => {

export interface DependenciesExtension {
dependencies: {
isSatisfied: (name: string, version: string) => boolean
isSatisfiedDev: (name: string, version: string) => boolean
exists: (name: string) => boolean
existsDev: (name: string) => boolean
add: (
name: string,
context: ProjectContext,
version?: string
options?: {
version?: string
skipInstalledCheck?: boolean
}
) => Promise<void>
addDev: (
name: string,
context: ProjectContext,
version?: string
options?: {
version?: string
skipInstalledCheck?: boolean
}
) => Promise<void>
}
}
10 changes: 8 additions & 2 deletions src/extensions/projectConfig.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,37 @@
import { CycliToolbox, PackageJson } from '../types'
import { AppJson, CycliToolbox, PackageJson } from '../types'

module.exports = (toolbox: CycliToolbox) => {
const { filesystem } = toolbox

const packageJson = (): PackageJson => {
if (!filesystem.exists('package.json')) {
throw Error(
'No package.json found in current directory. Are you sure you are in a project directory?'
'No package.json found in current directory. Are you sure you are in a project directory?'
)
}

return filesystem.read('package.json', 'json')
}

const appJson = (): AppJson | undefined => {
return filesystem.read('app.json', 'json')
}

const getName = (): string => {
return packageJson().name
}

toolbox.projectConfig = {
packageJson,
appJson,
getName,
}
}

export interface ProjectConfigExtension {
projectConfig: {
packageJson: () => PackageJson
appJson: () => AppJson | undefined
getName: () => string
}
}
6 changes: 5 additions & 1 deletion src/extensions/projectContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ module.exports = (toolbox: CycliToolbox) => {

if (packageJson.workspaces) {
throw Error(
'The current directory is workspace root directory. Please run the script again from selected package root directory.'
'The current directory is workspace root directory. Please run the script again from selected package root directory.'
)
}

Expand All @@ -61,6 +61,9 @@ module.exports = (toolbox: CycliToolbox) => {
const absFromRepoRoot = (...paths: string[]): string =>
join(repoRoot, ...paths)

const expoConfigJson = toolbox.filesystem.read('app.json', 'json')
km1chno marked this conversation as resolved.
Show resolved Hide resolved
const iOSAppName = expoConfigJson?.expo?.name.replaceAll('-', '')

return {
packageManager: getPackageManager(repoRoot),
path: {
Expand All @@ -69,6 +72,7 @@ module.exports = (toolbox: CycliToolbox) => {
relFromRepoRoot,
absFromRepoRoot,
},
iOSAppName,
selectedOptions: [],
}
}
Expand Down
6 changes: 6 additions & 0 deletions src/extensions/scripts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ module.exports = (toolbox: CycliToolbox) => {
const add = async (name: string, command: string) => {
await patching.update('package.json', (config) => {
if (config.scripts[name]) {
toolbox.interactive.warning(
[
`Skipping attempt to add script "${name}": "${command}" to package.json as script ${name} already exists.`,
`Consider updating it to make generated workflows work properly.`,
].join(' ')
)
return config
}

Expand Down
8 changes: 7 additions & 1 deletion src/extensions/workflows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ module.exports = (toolbox: CycliToolbox) => {
return `${toolbox.projectConfig.getName()}-${workflowBasename}.yml`
}

const formatWorkflowString = (workflowString: string): string => {
return workflowString
.trimStart() // Remove white characters from beginning
.replace(/\n([ ]*\n[ ]*)+\n/g, '\n\n') // Replace >=3 consecutive empty lines (possibly containing spaces) with two endlines
}

const generate = async (
template: string,
context: ProjectContext,
Expand All @@ -40,7 +46,7 @@ module.exports = (toolbox: CycliToolbox) => {
workflowFileName
)

toolbox.filesystem.write(target, workflowString.trimStart())
toolbox.filesystem.write(target, formatWorkflowString(workflowString))

toolbox.interactive.step(`Created ${workflowFileName} workflow file.`)
}
Expand Down
Loading