Skip to content

Commit b7a4cb8

Browse files
jankoritakclaude
andcommitted
fix(cli): load .env before reading token in notebooks rename
Mirrors the sync and run --cloud lifecycle: dotenv.config on the working directory's .env right before the DEEPNOTE_TOKEN read, with real environment variables keeping precedence over file values. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent d029c81 commit b7a4cb8

2 files changed

Lines changed: 62 additions & 6 deletions

File tree

‎packages/cli/src/commands/notebooks/rename-notebook.test.ts‎

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import fs from 'node:fs/promises'
2+
import os from 'node:os'
3+
import path from 'node:path'
14
import { ApiError } from '@deepnote/database-integrations'
25
import { Command } from 'commander'
36
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
@@ -94,6 +97,46 @@ describe('notebooks rename command', () => {
9497
)
9598
})
9699

100+
it('reads the token from a .env file in the working directory', async () => {
101+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'deepnote-rename-'))
102+
try {
103+
await fs.writeFile(path.join(dir, '.env'), 'DEEPNOTE_TOKEN=dotenv-token\n')
104+
vi.spyOn(process, 'cwd').mockReturnValue(dir)
105+
106+
await createNotebooksRenameAction(new Command())(NOTEBOOK_ID, 'Renamed', options({ token: undefined }))
107+
108+
expect(cloudMock.updateNotebook).toHaveBeenCalledWith(
109+
expect.any(String),
110+
'dotenv-token',
111+
NOTEBOOK_ID,
112+
expect.anything()
113+
)
114+
expect(process.exitCode).toBeUndefined()
115+
} finally {
116+
await fs.rm(dir, { recursive: true, force: true })
117+
}
118+
})
119+
120+
it('prefers a real environment variable over the .env file', async () => {
121+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'deepnote-rename-'))
122+
try {
123+
await fs.writeFile(path.join(dir, '.env'), 'DEEPNOTE_TOKEN=dotenv-token\n')
124+
vi.spyOn(process, 'cwd').mockReturnValue(dir)
125+
process.env.DEEPNOTE_TOKEN = 'env-token'
126+
127+
await createNotebooksRenameAction(new Command())(NOTEBOOK_ID, 'Renamed', options({ token: undefined }))
128+
129+
expect(cloudMock.updateNotebook).toHaveBeenCalledWith(
130+
expect.any(String),
131+
'env-token',
132+
NOTEBOOK_ID,
133+
expect.anything()
134+
)
135+
} finally {
136+
await fs.rm(dir, { recursive: true, force: true })
137+
}
138+
})
139+
97140
it('emits the stable JSON contract on success', async () => {
98141
await createNotebooksRenameAction(new Command())(NOTEBOOK_ID, 'Renamed', options({ output: 'json' }))
99142

@@ -117,11 +160,19 @@ describe('notebooks rename command', () => {
117160
})
118161

119162
it('exits with invalid usage when no token is available', async () => {
120-
await createNotebooksRenameAction(new Command())(NOTEBOOK_ID, 'Renamed', options({ token: undefined }))
121-
122-
expect(cloudMock.updateNotebook).not.toHaveBeenCalled()
123-
expect(process.exitCode).toEqual(ExitCode.InvalidUsage)
124-
expect(errorSpy.mock.calls.flat().join('\n')).not.toEqual('')
163+
// Run from an empty directory so a developer's own .env cannot supply a token.
164+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'deepnote-rename-'))
165+
try {
166+
vi.spyOn(process, 'cwd').mockReturnValue(dir)
167+
168+
await createNotebooksRenameAction(new Command())(NOTEBOOK_ID, 'Renamed', options({ token: undefined }))
169+
170+
expect(cloudMock.updateNotebook).not.toHaveBeenCalled()
171+
expect(process.exitCode).toEqual(ExitCode.InvalidUsage)
172+
expect(errorSpy.mock.calls.flat().join('\n')).not.toEqual('')
173+
} finally {
174+
await fs.rm(dir, { recursive: true, force: true })
175+
}
125176
})
126177

127178
it('treats API conflicts as invalid usage and surfaces the server message', async () => {

‎packages/cli/src/commands/notebooks/rename-notebook.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
import { join } from 'node:path'
12
import { updateNotebook } from '@deepnote/cloud'
2-
import { ApiError, DEFAULT_API_URL } from '@deepnote/database-integrations'
3+
import { ApiError, DEFAULT_API_URL, DEFAULT_ENV_FILE } from '@deepnote/database-integrations'
34
import type { Command } from 'commander'
5+
import dotenv from 'dotenv'
46
import { DEEPNOTE_TOKEN_ENV } from '../../constants'
57
import { ExitCode } from '../../exit-codes'
68
import { log, error as logError, outputJson } from '../../output'
@@ -57,6 +59,9 @@ async function renameNotebookInCloud(
5759
throw new InvalidRenameInputError('Notebook name cannot be empty.')
5860
}
5961

62+
// Load .env from the working directory before reading the token — mirrors `sync` and `run --cloud`.
63+
dotenv.config({ path: join(process.cwd(), DEFAULT_ENV_FILE), quiet: true })
64+
6065
const token = options.token?.trim() || process.env[DEEPNOTE_TOKEN_ENV]?.trim()
6166
if (!token) {
6267
throw new MissingTokenError()

0 commit comments

Comments
 (0)