diff --git a/dev/src/cli/deploy/cli_deploy_cloud_run.ts b/dev/src/cli/deploy/cli_deploy_cloud_run.ts index eeec5ddd6..27cf2b404 100644 --- a/dev/src/cli/deploy/cli_deploy_cloud_run.ts +++ b/dev/src/cli/deploy/cli_deploy_cloud_run.ts @@ -57,6 +57,17 @@ function validateGcloudExtraArgs( } } +// gcloud's --verbosity vocabulary is critical|debug|error|info|none|warning, +// which is not the ADK CLI's debug|info|warn|error. gcloud has no 'warn', so +// forwarding the ADK level verbatim makes gcloud reject the argument and fail +// the whole deploy. +const GCLOUD_VERBOSITY_BY_LOG_LEVEL: Record = { + 'debug': 'debug', + 'info': 'info', + 'warn': 'warning', + 'error': 'error', +}; + function prepareGCloudArguments(options: DeployToCloudRunOptions): string[] { const regionOptions: string[] = options.region ? ['--region', options.region] @@ -90,7 +101,7 @@ function prepareGCloudArguments(options: DeployToCloudRunOptions): string[] { '--port', options.port.toString(), '--verbosity', - options.logLevel.toLowerCase(), + GCLOUD_VERBOSITY_BY_LOG_LEVEL[options.logLevel.toLowerCase()] ?? 'info', ]; if (options.a2aAuthToken) { diff --git a/dev/test/cli/cli_deploy_cloud_run_test.ts b/dev/test/cli/cli_deploy_cloud_run_test.ts index e24ff4bc3..81c9fe673 100644 --- a/dev/test/cli/cli_deploy_cloud_run_test.ts +++ b/dev/test/cli/cli_deploy_cloud_run_test.ts @@ -18,6 +18,7 @@ import { isFile, isFolderExists, loadFileData, + saveToFile, tryToFindFileRecursively, } from '../../src/utils/file_utils.js'; @@ -478,6 +479,54 @@ describe('deployToCloudRun', () => { ); }); + function verbosityArg(): string { + const gcloudArgs = spawnMock.mock.calls[0][1]; + return gcloudArgs[gcloudArgs.indexOf('--verbosity') + 1]; + } + + it('should translate the warn log level to the gcloud warning verbosity', async () => { + await deployToCloudRun({...defaultOptions, logLevel: 'warn'}); + + expect(verbosityArg()).toBe('warning'); + expect(spawnMock.mock.calls[0][1]).not.toContain('warn'); + }); + + it.each(['debug', 'info', 'error'])( + 'should pass the %s log level through to gcloud unchanged', + async (logLevel) => { + await deployToCloudRun({...defaultOptions, logLevel}); + + expect(verbosityArg()).toBe(logLevel); + }, + ); + + it('should accept an upper-case log level', async () => { + await deployToCloudRun({...defaultOptions, logLevel: 'WARN'}); + + expect(verbosityArg()).toBe('warning'); + }); + + it('should fall back to the info verbosity for a level gcloud does not accept', async () => { + await deployToCloudRun({...defaultOptions, logLevel: 'trace'}); + + expect(verbosityArg()).toBe('info'); + }); + + it('should keep the raw ADK log level in the generated Dockerfile', async () => { + // The containerized ADK server speaks the ADK vocabulary, so it must keep + // receiving 'warn' and never the gcloud spelling. + await deployToCloudRun({...defaultOptions, logLevel: 'warn'}); + + // createPackageJson also writes through saveToFile, so select by path. + const dockerFileCall = vi + .mocked(saveToFile) + .mock.calls.find(([filePath]) => filePath.endsWith('Dockerfile')); + if (!dockerFileCall) { + expect.fail('deployToCloudRun did not write a Dockerfile'); + } + expect(dockerFileCall[1]).toContain("--log_level='warn'"); + }); + it('should handle spawn failures', async () => { const consoleErrorSpy = vi.spyOn(console, 'error'); spawnMock.mockReturnValue({