Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 3 additions & 2 deletions src/commands/env/env-clone.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { OptionValues } from 'commander'
import { chalk, log, logAndThrowError } from '../../utils/command-helpers.js'
import { promptEnvCloneOverwrite } from '../../utils/prompts/env-clone-prompt.js'
import BaseCommand from '../base-command.js'
import { failNotLinked } from './utils.js'

// @ts-expect-error TS(7006) FIXME: Parameter 'api' implicitly has an 'any' type.
const safeGetSite = async (api, siteId) => {
Expand Down Expand Up @@ -60,10 +61,10 @@ export const envClone = async (options: OptionValues, command: BaseCommand) => {
const { force } = options

if (!site.id && !options.from) {
log(
return failNotLinked(
options,
'Please include the source project ID as the `--from` option, or run `netlify link` to link this folder to a Netlify project',
)
return false
}

const sourceId = options.from || site.id
Expand Down
9 changes: 4 additions & 5 deletions src/commands/env/env-get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,15 @@ import { OptionValues } from 'commander'
import { chalk, log, logJson } from '../../utils/command-helpers.js'
import { SUPPORTED_CONTEXTS, getEnvelopeEnv } from '../../utils/env/index.js'
import BaseCommand from '../base-command.js'
import { getSiteInfo } from './utils.js'
import { failNotLinked, getEnvSiteId, getSiteInfo } from './utils.js'

export const envGet = async (name: string, options: OptionValues, command: BaseCommand) => {
const { context, scope } = options
const { api, cachedConfig, site } = command.netlify
const siteId = site.id
const { api, cachedConfig } = command.netlify
const siteId = getEnvSiteId(options, command)

if (!siteId) {
log('No project id found, please run inside a project folder or `netlify link`')
return false
return failNotLinked(options)
}

const siteInfo = await getSiteInfo(api, siteId, cachedConfig)
Expand Down
9 changes: 4 additions & 5 deletions src/commands/env/env-import.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import dotenv from 'dotenv'
import { exit, log, logJson } from '../../utils/command-helpers.js'
import { translateFromEnvelopeToMongo, translateFromMongoToEnvelope } from '../../utils/env/index.js'
import BaseCommand from '../base-command.js'
import { getSiteInfo } from './utils.js'
import { failNotLinked, getEnvSiteId, getSiteInfo } from './utils.js'

/**
* Saves the imported env in the Envelope service
Expand Down Expand Up @@ -51,12 +51,11 @@ const importDotEnv = async ({ api, importedEnv, options, siteInfo }) => {
}

export const envImport = async (fileName: string, options: OptionValues, command: BaseCommand) => {
const { api, cachedConfig, site } = command.netlify
const siteId = site.id
const { api, cachedConfig } = command.netlify
const siteId = getEnvSiteId(options, command)

if (!siteId) {
log('No project id found, please run inside a project folder or `netlify link`')
return false
return failNotLinked(options)
}

const siteInfo = await getSiteInfo(api, siteId, cachedConfig)
Expand Down
9 changes: 4 additions & 5 deletions src/commands/env/env-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { chalk, log, logJson } from '../../utils/command-helpers.js'
import { SUPPORTED_CONTEXTS, getEnvelopeEnv, getHumanReadableScopes } from '../../utils/env/index.js'
import type BaseCommand from '../base-command.js'
import { EnvironmentVariables } from '../../utils/types.js'
import { getSiteInfo } from './utils.js'
import { failNotLinked, getEnvSiteId, getSiteInfo } from './utils.js'

const MASK_LENGTH = 50
const MASK = '*'.repeat(MASK_LENGTH)
Expand Down Expand Up @@ -43,12 +43,11 @@ const getTable = ({

export const envList = async (options: OptionValues, command: BaseCommand) => {
const { context, scope } = options
const { api, cachedConfig, site } = command.netlify
const siteId = site.id
const { api, cachedConfig } = command.netlify
const siteId = getEnvSiteId(options, command)

if (!siteId) {
log('No project id found, please run inside a project folder or `netlify link`')
return false
return failNotLinked(options)
}

const siteInfo = await getSiteInfo(api, siteId, cachedConfig)
Expand Down
9 changes: 4 additions & 5 deletions src/commands/env/env-set.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { chalk, logAndThrowError, log, logJson } from '../../utils/command-helpe
import { SUPPORTED_CONTEXTS, ALL_ENVELOPE_SCOPES, translateFromEnvelopeToMongo } from '../../utils/env/index.js'
import { promptOverwriteEnvVariable } from '../../utils/prompts/env-set-prompts.js'
import BaseCommand from '../base-command.js'
import { getSiteInfo } from './utils.js'
import { failNotLinked, getEnvSiteId, getSiteInfo } from './utils.js'

/**
* Updates the env for a site configured with Envelope with a new key/value pair
Expand Down Expand Up @@ -110,11 +110,10 @@ const setInEnvelope = async ({ api, context, force, key, scope, secret, siteInfo

export const envSet = async (key: string, value: string, options: OptionValues, command: BaseCommand) => {
const { context, force, scope, secret } = options
const { api, cachedConfig, site } = command.netlify
const siteId = site.id
const { api, cachedConfig } = command.netlify
const siteId = getEnvSiteId(options, command)
if (!siteId) {
log('No project id found, please run inside a project folder or `netlify link`')
return false
return failNotLinked(options)
}
const siteInfo = await getSiteInfo(api, siteId, cachedConfig)

Expand Down
9 changes: 4 additions & 5 deletions src/commands/env/env-unset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { chalk, log, logJson } from '../../utils/command-helpers.js'
import { SUPPORTED_CONTEXTS, translateFromEnvelopeToMongo } from '../../utils/env/index.js'
import { promptOverwriteEnvVariable } from '../../utils/prompts/env-unset-prompts.js'
import BaseCommand from '../base-command.js'
import { getSiteInfo } from './utils.js'
import { failNotLinked, getEnvSiteId, getSiteInfo } from './utils.js'
/**
* Deletes a given key from the env of a site configured with Envelope
* @returns {Promise<object>}
Expand Down Expand Up @@ -70,12 +70,11 @@ const unsetInEnvelope = async ({ api, context, force, key, siteInfo }) => {

export const envUnset = async (key: string, options: OptionValues, command: BaseCommand) => {
const { context, force } = options
const { api, cachedConfig, site } = command.netlify
const siteId = site.id
const { api, cachedConfig } = command.netlify
const siteId = getEnvSiteId(options, command)

if (!siteId) {
log('No project id found, please run inside a project folder or `netlify link`')
return false
return failNotLinked(options)
}

const siteInfo = await getSiteInfo(api, siteId, cachedConfig)
Expand Down
18 changes: 18 additions & 0 deletions src/commands/env/utils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import type { NetlifyAPI } from '@netlify/api'
import type { OptionValues } from 'commander'

import type { CachedConfig } from '../../lib/build.js'
import { exit, logJson } from '../../utils/command-helpers.js'
import type { SiteInfo } from '../../utils/types.js'
import type BaseCommand from '../base-command.js'

export const getSiteInfo = async (api: NetlifyAPI, siteId: string, cachedConfig: CachedConfig): Promise<SiteInfo> => {
const { siteInfo: cachedSiteInfo } = cachedConfig
Expand All @@ -10,3 +13,18 @@ export const getSiteInfo = async (api: NetlifyAPI, siteId: string, cachedConfig:
}
return cachedSiteInfo
}

export const getEnvSiteId = (options: OptionValues, command: BaseCommand): string | undefined =>
options.site ? command.netlify.siteInfo.id : command.netlify.site.id

export const failNotLinked = (
options: OptionValues,
message = 'No project id found, please run inside a project folder or `netlify link`',
): never => {
if (options.json) {
logJson({ error: { code: 'NOT_LINKED', message, fix: 'netlify link' } })
} else {
process.stderr.write(`${message}\n`)
}
return exit(1)
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`commands/env > env:clone > should exit if the folder is not linked to a project, and --from is not provided 1`] = `"Please include the source project ID as the \`--from\` option, or run \`netlify link\` to link this folder to a Netlify project"`;

exports[`commands/env > env:clone > should return success message 1`] = `
"Successfully cloned environment variables from site-name to site-name-a
Changes will require a redeploy to take effect on any deployed versions of your project."
Expand Down
11 changes: 7 additions & 4 deletions tests/integration/commands/env/env.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -442,19 +442,22 @@ describe('commands/env', () => {
})
})

test('should exit if the folder is not linked to a project, and --from is not provided', async (t) => {
test('should exit non-zero if the folder is not linked to a project, and --from is not provided', async (t) => {
await withSiteBuilder(t, async (builder) => {
await builder.build()

const cliResponse = (await callCli(['env:clone', '--to', 'site_id_a', '--force'], {
const error = (await callCli(['env:clone', '--to', 'site_id_a', '--force'], {
cwd: builder.directory,
extendEnv: false,
env: {
PATH: process.env.PATH,
},
})) as string
}).catch((error_: unknown) => error_)) as { exitCode: number; message: string }

t.expect(normalize(cliResponse)).toMatchSnapshot()
t.expect(error.exitCode).toBe(1)
t.expect(error.message).toContain(
'Please include the source project ID as the `--from` option, or run `netlify link`',
)
})
})

Expand Down
108 changes: 108 additions & 0 deletions tests/unit/commands/env/env-get.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { describe, expect, test, vi, beforeEach, afterEach, type MockInstance } from 'vitest'

const { mockGetEnvelopeEnv, jsonMessages, logMessages } = vi.hoisted(() => ({
mockGetEnvelopeEnv: vi.fn(),
jsonMessages: [] as unknown[],
logMessages: [] as string[],
}))

vi.mock('../../../../src/utils/command-helpers.js', async () => ({
...(await vi.importActual('../../../../src/utils/command-helpers.js')),
log: (...args: string[]) => {
logMessages.push(args.join(' '))
},
logJson: (message: unknown) => {
jsonMessages.push(message)
},
exit: (code = 0): never => {
throw new Error(`process.exit(${String(code)})`)
},
}))

vi.mock('../../../../src/utils/env/index.js', async () => ({
...(await vi.importActual('../../../../src/utils/env/index.js')),
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
getEnvelopeEnv: (...args: unknown[]) => mockGetEnvelopeEnv(...args),
}))

import { envGet } from '../../../../src/commands/env/env-get.js'
import type BaseCommand from '../../../../src/commands/base-command.js'

const createMockCommand = ({ linkedSiteId, flagSiteId }: { linkedSiteId?: string; flagSiteId?: string } = {}) => {
const api = { getSite: vi.fn().mockResolvedValue({ id: flagSiteId, name: 'flag-site' }) }
const command = {
netlify: {
api,
cachedConfig: { env: {}, siteInfo: { id: linkedSiteId } },
site: { id: linkedSiteId },
siteInfo: { id: flagSiteId },
},
} as unknown as BaseCommand
return { api, command }
}

describe('envGet', () => {
let stderrSpy: MockInstance<typeof process.stderr.write>

beforeEach(() => {
jsonMessages.length = 0
logMessages.length = 0
vi.clearAllMocks()
mockGetEnvelopeEnv.mockResolvedValue({ FOO: { value: 'bar' } })
stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true)
})

afterEach(() => {
stderrSpy.mockRestore()
})

test('prints a NOT_LINKED JSON envelope and exits non-zero when unlinked with --json', async () => {
const { command } = createMockCommand()

await expect(envGet('FOO', { json: true }, command)).rejects.toThrowError('process.exit(1)')

expect(jsonMessages).toHaveLength(1)
expect(jsonMessages[0]).toMatchObject({ error: { code: 'NOT_LINKED', fix: 'netlify link' } })
expect(mockGetEnvelopeEnv).not.toHaveBeenCalled()
})

test('prints prose to stderr and exits non-zero when unlinked without --json', async () => {
const { command } = createMockCommand()

await expect(envGet('FOO', {}, command)).rejects.toThrowError('process.exit(1)')

expect(stderrSpy).toHaveBeenCalledWith(
'No project id found, please run inside a project folder or `netlify link`\n',
)
expect(jsonMessages).toHaveLength(0)
})

test('returns the variable for the linked site without --site', async () => {
const { command } = createMockCommand({ linkedSiteId: 'linked-id' })

await envGet('FOO', { json: true, context: 'dev', scope: 'any' }, command)

expect(jsonMessages).toEqual([{ FOO: 'bar' }])
})

test('honors --site by using the resolved siteInfo instead of the linked state', async () => {
const { api, command } = createMockCommand({ flagSiteId: 'flag-site-id' })

await envGet('FOO', { json: true, context: 'dev', scope: 'any', site: 'flag-site-id' }, command)

expect(api.getSite).toHaveBeenCalledWith({ siteId: 'flag-site-id' })
const [envelopeArgs] = mockGetEnvelopeEnv.mock.calls[0] as [{ siteInfo: { id: string } }]
expect(envelopeArgs.siteInfo.id).toBe('flag-site-id')
expect(jsonMessages).toEqual([{ FOO: 'bar' }])
})

test('prefers --site over the linked site id', async () => {
const { api, command } = createMockCommand({ linkedSiteId: 'linked-id', flagSiteId: 'flag-site-id' })

await envGet('FOO', { json: true, context: 'dev', scope: 'any', site: 'flag-site-id' }, command)

expect(api.getSite).toHaveBeenCalledWith({ siteId: 'flag-site-id' })
const [envelopeArgs] = mockGetEnvelopeEnv.mock.calls[0] as [{ siteInfo: { id: string } }]
expect(envelopeArgs.siteInfo.id).toBe('flag-site-id')
})
})
Loading
Loading