Skip to content

Commit

Permalink
fix(internals, migrate): split isCi into isInteractive (#15008)
Browse files Browse the repository at this point in the history
Co-authored-by: Alberto Schiabel <jkomyno@users.noreply.github.com>
  • Loading branch information
Jolg42 and jkomyno committed Aug 31, 2022
1 parent b39cf85 commit a1441cb
Show file tree
Hide file tree
Showing 14 changed files with 155 additions and 75 deletions.
1 change: 0 additions & 1 deletion packages/internals/package.json
Expand Up @@ -61,7 +61,6 @@
"global-dirs": "3.0.0",
"globby": "11.1.0",
"has-yarn": "2.1.0",
"is-ci": "3.0.1",
"is-windows": "^1.0.2",
"is-wsl": "^2.2.0",
"make-dir": "3.1.0",
Expand Down
2 changes: 2 additions & 0 deletions packages/internals/src/index.ts
Expand Up @@ -59,6 +59,8 @@ export { getEnvPaths } from './utils/getEnvPaths'
export { version } from './utils/getVersionFromPackageJson'
export { handlePanic } from './utils/handlePanic'
export { isCi } from './utils/isCi'
export { isInteractive } from './utils/isInteractive'
export { canPrompt } from './utils/canPrompt'
export { isCurrentBinInstalledGlobally } from './utils/isCurrentBinInstalledGlobally'
export { jestConsoleContext, jestContext, jestProcessContext } from './utils/jestContext'
export { keyBy } from './utils/keyBy'
Expand Down
79 changes: 31 additions & 48 deletions packages/internals/src/utils/__tests__/isCi.test.ts
@@ -1,68 +1,51 @@
import { isCi } from '../isCi'

// This allows us to override the return value of `is-ci`. The getter method
// is required because the package exports a value, not a function, and we want
// to be able to control it in tests.
const mockValue = jest.fn().mockReturnValue(false)
jest.mock('is-ci', () => ({
get isCi() {
return mockValue()
},
}))
const originalEnv = { ...process.env }
const originalStdinisTTY = process.stdin.isTTY

const temporarilySet = (object: any, prop: string, value: unknown) => {
const original = object[prop]
describe('isCi', () => {
beforeEach(() => {
setValueOnProcess(object, prop, value)
process.env = originalEnv
process.stdin.isTTY = originalStdinisTTY
})
afterEach(() => {
setValueOnProcess(object, prop, original)
afterAll(() => {
process.env = originalEnv
process.stdin.isTTY = originalStdinisTTY
})
}

// If you set undefined in process.env it stringifies it, so we
// need special handling for that case of "unsetting" the process.env.
const setValueOnProcess = (object: any, prop: string, value: unknown) => {
if (object === process.env && value === undefined) {
delete object[prop]
} else {
object[prop] = value
}
}

describe('isCi', () => {
describe('when outside a TTY environment', () => {
temporarilySet(process.stdin, 'isTTY', false)
describe('in non TTY environment', () => {
beforeEach(() => {
delete process.env.BUILDKITE
delete process.env.GITHUB_ACTIONS
delete process.env.CI
process.stdin.isTTY = false
})

test('returns false', () => {
expect(isCi()).toBe(true)
test('with undefined env vars, isCi should be false', () => {
expect(isCi()).toBe(false)
})
})

describe('when in TTY environment', () => {
temporarilySet(process.stdin, 'isTTY', true)
describe('in TTY environment', () => {
beforeEach(() => {
delete process.env.BUILDKITE
delete process.env.GITHUB_ACTIONS
delete process.env.CI
process.stdin.isTTY = true
})

test('when isCiLib tells us so', () => {
mockValue.mockReturnValueOnce(true)
test('with CI env var, isCi should be true', () => {
process.env.CI = 'true'
expect(isCi()).toBe(true)
})

describe('with GitHub Actions env var', () => {
temporarilySet(process.env, 'GITHUB_ACTIONS', 'true')

test('returns true', () => {
expect(isCi()).toBe(true)
})
test('with GitHub Actions env var, isCi should be true', () => {
process.env.GITHUB_ACTIONS = 'true'
expect(isCi()).toBe(true)
})

describe('outside a CI environment, with TTY', () => {
temporarilySet(process.stdin, 'isTTY', true)
temporarilySet(process.env, 'GITHUB_ACTIONS', undefined)

test('returns false', () => {
mockValue.mockReturnValueOnce(false)
expect(isCi()).toBe(false)
})
test('with undefined env vars, isCi should be false', () => {
expect(isCi()).toBe(false)
})
})
})
36 changes: 36 additions & 0 deletions packages/internals/src/utils/__tests__/isInteractive.test.ts
@@ -0,0 +1,36 @@
import { isInteractive } from '../isInteractive'

const originalEnv = { ...process.env }

describe('isInteractive', () => {
beforeEach(() => {
process.env = { ...originalEnv }
})
afterAll(() => {
process.env = { ...originalEnv }
})

describe('in non TTY environment', () => {
const mockedValue = { isTTY: false } as NodeJS.ReadStream
test('isInteractive should be false', () => {
expect(isInteractive({ stream: mockedValue })).toBe(false)
})

test('isInteractive should be false if TERM = dumb', () => {
process.env.TERM = 'dumb'
expect(isInteractive({ stream: mockedValue })).toBe(false)
})
})

describe('in TTY environment', () => {
const mockedValue = { isTTY: true } as NodeJS.ReadStream
test('isInteractive should be true', () => {
expect(isInteractive({ stream: mockedValue })).toBe(true)
})

test('isInteractive should be false if TERM = dumb', () => {
process.env.TERM = 'dumb'
expect(isInteractive({ stream: mockedValue })).toBe(false)
})
})
})
12 changes: 12 additions & 0 deletions packages/internals/src/utils/canPrompt.ts
@@ -0,0 +1,12 @@
import prompt from 'prompts'
import { isCi } from './isCi'
import { isInteractive } from './isInteractive'

// If not TTY or in CI we want to throw an error and not prompt.
// Because:
// Prompting when non interactive is not possible.
// Prompting in CI would hang forever / until a timeout occurs.
export const canPrompt = (): boolean => {
// Note: We use prompts.inject() for testing prompts in our CI
return Boolean((prompt as any)._injected?.length) === true || (isInteractive() && !isCi())
}
4 changes: 2 additions & 2 deletions packages/internals/src/utils/handlePanic.ts
Expand Up @@ -3,8 +3,8 @@ import prompt from 'prompts'

import type { RustPanic } from '../panic'
import { sendPanic } from '../sendPanic'
import { canPrompt } from './canPrompt'
import { wouldYouLikeToCreateANewIssue } from './getGithubIssueUrl'
import { isCi } from './isCi'
import { link } from './link'

export async function handlePanic(
Expand All @@ -13,7 +13,7 @@ export async function handlePanic(
engineVersion: string,
command: string,
): Promise<void> {
if (isCi() && Boolean((prompt as any)._injected?.length) === false) {
if (!canPrompt()) {
throw error
}

Expand Down
53 changes: 50 additions & 3 deletions packages/internals/src/utils/isCi.ts
@@ -1,5 +1,52 @@
import { isCi as isCiLib } from 'is-ci'

// Returns true if the current environment is a CI environment.
export const isCi = (): boolean => {
return !process.stdin.isTTY || isCiLib || Boolean(process.env.GITHUB_ACTIONS)
const env = process.env

// From https://github.com/watson/ci-info/blob/44e98cebcdf4403f162195fbcf90b1f69fc6e047/index.js#L54-L61
// Evaluating at runtime makes it possible to change the values in our tests
// This list is probably not exhaustive though `process.env.CI` should be enough
// but since we were using this utility in the past, we want to keep the same behavior
return !!(
env.CI || // Travis CI, CircleCI, Cirrus CI, Gitlab CI, Appveyor, CodeShip, dsari
env.CONTINUOUS_INTEGRATION || // Travis CI, Cirrus CI
env.BUILD_NUMBER || // Jenkins, TeamCity
env.RUN_ID || // TaskCluster, dsari
// From `env` from https://github.com/watson/ci-info/blob/44e98cebcdf4403f162195fbcf90b1f69fc6e047/vendors.json
env.APPVEYOR ||
env.SYSTEM_TEAMFOUNDATIONCOLLECTIONURI ||
env.AC_APPCIRCLE ||
env.bamboo_planKey ||
env.BITBUCKET_COMMIT ||
env.BITRISE_IO ||
env.BUILDKITE ||
env.CIRCLECI ||
env.CIRRUS_CI ||
env.CODEBUILD_BUILD_ARN ||
env.CF_BUILD_ID ||
env.CI_NAME ||
env.DRONE ||
env.DSARI ||
env.EAS_BUILD ||
env.GITHUB_ACTIONS ||
env.GITLAB_CI ||
env.GOCD ||
env.LAYERCI ||
env.HUDSON ||
env.JENKINS ||
env.MAGNUM ||
env.NETLIFY ||
env.NEVERCODE ||
env.RENDER ||
env.SAILCI ||
env.SEMAPHORE ||
env.SCREWDRIVER ||
env.SHIPPABLE ||
env.TDDIUM ||
env.STRIDER ||
env.TEAMCITY_VERSION ||
env.TRAVIS ||
env.NOW_BUILDER ||
env.APPCENTER_BUILD_ID ||
false
)
}
6 changes: 6 additions & 0 deletions packages/internals/src/utils/isInteractive.ts
@@ -0,0 +1,6 @@
// Same logic as https://github.com/sindresorhus/is-interactive/blob/dc8037ae1a61d828cfb42761c345404055b1e036/index.js
// But defaults to check `stdin` for our prompts
// It checks that the stream is TTY, not a dumb terminal
export const isInteractive = ({ stream = process.stdin } = {}): boolean => {
return Boolean(stream && stream.isTTY && process.env.TERM !== 'dumb')
}
5 changes: 2 additions & 3 deletions packages/migrate/src/commands/DbDrop.ts
@@ -1,12 +1,12 @@
import {
arg,
canPrompt,
checkUnsupportedDataProxy,
Command,
dropDatabase,
format,
getSchemaDir,
HelpError,
isCi,
isError,
link,
loadEnvFile,
Expand Down Expand Up @@ -98,8 +98,7 @@ ${chalk.bold('Examples')}
console.info() // empty line

if (!args['--force']) {
// We use prompts.inject() for testing in our CI
if (isCi() && Boolean((prompt as any)._injected?.length) === false) {
if (!canPrompt()) {
throw new DbNeedsForceError('drop')
}

Expand Down
8 changes: 3 additions & 5 deletions packages/migrate/src/commands/DbPush.ts
@@ -1,12 +1,12 @@
import {
arg,
canPrompt,
checkUnsupportedDataProxy,
Command,
format,
formatms,
getCommandWithExecutor,
HelpError,
isCi,
isError,
loadEnvFile,
logger,
Expand Down Expand Up @@ -153,8 +153,7 @@ You can now remove the ${chalk.red('--preview-feature')} flag.`)
}
console.info() // empty line

// We use prompts.inject() for testing in our CI
if (isCi() && Boolean((prompt as any)._injected?.length) === false) {
if (!canPrompt()) {
migrate.stop()
throw new Error(`${messages.join('\n')}\n
Use the --force-reset flag to drop the database before push like ${chalk.bold.greenBright(
Expand Down Expand Up @@ -211,8 +210,7 @@ ${chalk.bold.redBright('All data will be lost.')}
console.info() // empty line

if (!args['--accept-data-loss']) {
// We use prompts.inject() for testing in our CI
if (isCi() && Boolean((prompt as any)._injected?.length) === false) {
if (!canPrompt()) {
migrate.stop()
throw new DbPushIgnoreWarningsWithFlagError()
}
Expand Down
8 changes: 3 additions & 5 deletions packages/migrate/src/commands/MigrateDev.ts
@@ -1,6 +1,7 @@
import Debug from '@prisma/debug'
import {
arg,
canPrompt,
checkUnsupportedDataProxy,
Command,
format,
Expand All @@ -9,7 +10,6 @@ import {
getDMMF,
getSchemaPath,
HelpError,
isCi,
isError,
loadEnvFile,
} from '@prisma/internals'
Expand Down Expand Up @@ -146,8 +146,7 @@ ${chalk.bold('Examples')}

if (devDiagnostic.action.tag === 'reset') {
if (!args['--force']) {
// We use prompts.inject() for testing in our CI
if (isCi() && Boolean((prompt as any)._injected?.length) === false) {
if (!canPrompt()) {
migrate.stop()
throw new MigrateDevEnvNonInteractiveError()
}
Expand Down Expand Up @@ -223,8 +222,7 @@ ${chalk.bold('Examples')}
console.info() // empty line

if (!args['--force']) {
// We use prompts.inject() for testing in our CI
if (isCi() && Boolean((prompt as any)._injected?.length) === false) {
if (!canPrompt()) {
migrate.stop()
throw new MigrateDevEnvNonInteractiveError()
}
Expand Down
5 changes: 2 additions & 3 deletions packages/migrate/src/commands/MigrateReset.ts
@@ -1,11 +1,11 @@
import {
arg,
canPrompt,
checkUnsupportedDataProxy,
Command,
format,
getSchemaPath,
HelpError,
isCi,
isError,
loadEnvFile,
} from '@prisma/internals'
Expand Down Expand Up @@ -103,8 +103,7 @@ ${chalk.bold('Examples')}

console.info() // empty line
if (!args['--force']) {
// We use prompts.inject() for testing in our CI
if (isCi() && Boolean((prompt as any)._injected?.length) === false) {
if (!canPrompt()) {
throw new MigrateResetEnvNonInteractiveError()
}

Expand Down
5 changes: 3 additions & 2 deletions packages/migrate/src/utils/promptForMigrationName.ts
@@ -1,4 +1,4 @@
import { isCi } from '@prisma/internals'
import { isCi, isInteractive } from '@prisma/internals'
import slugify from '@sindresorhus/slugify'
import { prompt } from 'prompts'

Expand All @@ -17,7 +17,8 @@ export async function getMigrationName(name?: string): Promise<getMigratioNameOu
}
}
// We use prompts.inject() for testing in our CI
else if (isCi() && Boolean(prompt._injected?.length) === false) {
// If not TTY or CI, use default name
else if ((!isInteractive || isCi()) && Boolean(prompt._injected?.length) === false) {
return {
name: '',
}
Expand Down

0 comments on commit a1441cb

Please sign in to comment.