From ecb015952ee4ec2387a67e7c935d7ca7d076feb5 Mon Sep 17 00:00:00 2001 From: Pierre Millot Date: Wed, 17 Sep 2025 15:11:08 +0200 Subject: [PATCH 1/4] chore(scripts): add an option to attach a java debugger --- docker-compose.yml | 4 +-- scripts/buildLanguages.ts | 5 ++- scripts/cli/index.ts | 56 +++++++++++++++++++--------------- scripts/common.ts | 42 ++++++++++++++++++++----- scripts/cts/generate.ts | 8 +++-- scripts/docker/Dockerfile.base | 2 ++ scripts/docs/generate.ts | 8 +++-- scripts/generate.ts | 4 +-- 8 files changed, 89 insertions(+), 40 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 51082815d63..534b530d1f9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,8 +14,8 @@ services: - PYTHON_VERSION=${PYTHON_VERSION} command: tail -f /dev/null volumes: [./:/app] - # ports: - # - "5009:5009" # So we can debug the OpenAPI Generator process + ports: + - "5009:5009" # So we can debug the OpenAPI Generator process ruby: container_name: apic_ruby diff --git a/scripts/buildLanguages.ts b/scripts/buildLanguages.ts index 3bfdb5603ee..b5e60b0621c 100644 --- a/scripts/buildLanguages.ts +++ b/scripts/buildLanguages.ts @@ -69,7 +69,10 @@ async function buildLanguage(language: Language, gens: Generator[], buildType: B case 'kotlin': // the playground specify search but it will still build everything const isTestClass = buildType === 'guides' || buildType === 'snippets'; - await run(`./gradle/gradlew -p ${cwd} ${isTestClass ? 'testClasses' : 'assemble'} ${language == 'kotlin' ? '-Pclient=Search' : ''}`, { language }); + await run( + `./gradle/gradlew -p ${cwd} ${isTestClass ? 'testClasses' : 'assemble'} ${language == 'kotlin' ? '-Pclient=Search' : ''}`, + { language }, + ); break; case 'php': // await runComposerInstall(); diff --git a/scripts/cli/index.ts b/scripts/cli/index.ts index f777a807440..75cf1e6c76e 100644 --- a/scripts/cli/index.ts +++ b/scripts/cli/index.ts @@ -1,4 +1,4 @@ -import { Argument, program } from 'commander'; +import { Argument, Option, program } from 'commander'; import semver from 'semver'; import { buildLanguages } from '../buildLanguages.ts'; @@ -29,10 +29,11 @@ const args = { }; const flags = { - verbose: { - flag: '-v, --verbose', - description: 'make the generation verbose', - }, + verbose: new Option('-v, --verbose', 'make the generation verbose'), + debugger: new Option( + '-d, --debugger', + 'runs the generator in debug mode, it will wait for a Java debugger to be attached', + ), }; program.name('cli'); @@ -49,8 +50,9 @@ program .description('Generate a specified client') .addArgument(args.language) .addArgument(args.clients) - .option(flags.verbose.flag, flags.verbose.description) - .action(async (langArg: LangArg, clientArg: string[], { verbose }) => { + .addOption(flags.verbose) + .addOption(flags.debugger) + .action(async (langArg: LangArg, clientArg: string[], { verbose, debugger: withDebugger }) => { const { language, client, clientList } = transformSelection({ langArg, clientArg, @@ -58,7 +60,9 @@ program setVerbose(Boolean(verbose)); - await generate(generatorList({ language, client, clientList })); + console.log({ withDebugger }); + + await generate(generatorList({ language, client, clientList }), Boolean(withDebugger)); }); const buildCommand = program.command('build').description('Build the clients or specs'); @@ -68,7 +72,7 @@ buildCommand .description('Build a specified client') .addArgument(args.language) .addArgument(args.clients) - .option(flags.verbose.flag, flags.verbose.description) + .addOption(flags.verbose) .action(async (langArg: LangArg, clientArg: string[], { verbose }) => { const { language, client, clientList } = transformSelection({ langArg, @@ -85,7 +89,7 @@ buildCommand .description('Build a specified playground') .addArgument(args.language) .addArgument(args.clients) - .option(flags.verbose.flag, flags.verbose.description) + .addOption(flags.verbose) .action(async (langArg: LangArg, clientArg: string[], { verbose }) => { const { language, client, clientList } = transformSelection({ langArg, @@ -102,7 +106,7 @@ buildCommand .description('Build a specified snippets') .addArgument(args.language) .addArgument(args.clients) - .option(flags.verbose.flag, flags.verbose.description) + .addOption(flags.verbose) .action(async (langArg: LangArg, clientArg: string[], { verbose }) => { const { language, client, clientList } = transformSelection({ langArg, @@ -119,7 +123,7 @@ buildCommand .description('Build a specified guides') .addArgument(args.language) .addArgument(args.clients) - .option(flags.verbose.flag, flags.verbose.description) + .addOption(flags.verbose) .action(async (langArg: LangArg, clientArg: string[], { verbose }) => { const { language, client, clientList } = transformSelection({ langArg, @@ -135,7 +139,7 @@ buildCommand .command('specs') .description('Build a specified spec') .addArgument(args.clients) - .option(flags.verbose.flag, flags.verbose.description) + .addOption(flags.verbose) .option('-s, --skip-cache', 'skip cache checking to force building specs') .option('-j, --json', 'outputs the spec in JSON instead of yml') .option('-d, --docs', 'generates the doc specs with the code snippets') @@ -172,9 +176,10 @@ ctsCommand .description('Generate the CTS tests') .addArgument(args.language) .addArgument(args.clients) - .option(flags.verbose.flag, flags.verbose.description) + .addOption(flags.verbose) + .addOption(flags.debugger) .option('--lv, --language-version ', 'the version of the language to use') - .action(async (langArg: LangArg, clientArg: string[], { verbose, languageVersion }) => { + .action(async (langArg: LangArg, clientArg: string[], { verbose, debugger: withDebugger, languageVersion }) => { const { language, client, clientList } = transformSelection({ langArg, clientArg, @@ -182,7 +187,7 @@ ctsCommand setVerbose(Boolean(verbose)); - await ctsGenerateMany(generatorList({ language, client, clientList }), languageVersion); + await ctsGenerateMany(generatorList({ language, client, clientList }), withDebugger, languageVersion); }); ctsCommand @@ -190,7 +195,7 @@ ctsCommand .description('Run the tests for the CTS') .addArgument(args.language) .addArgument(args.clients) - .option(flags.verbose.flag, flags.verbose.description) + .addOption(flags.verbose) .option('-e, --no-e2e', 'skip the e2e tests, that requires internet connection') .option('-c, --no-client', 'skip the client tests') .option('-r, --no-requests', 'skip the requests tests') @@ -250,7 +255,7 @@ program .description('Format the specified folder for a specific language') .addArgument(args.requiredLanguage) .argument('folder', 'The folder to format') - .option(flags.verbose.flag, flags.verbose.description) + .addOption(flags.verbose) .action(async (language: string, folder: string, { verbose }) => { setVerbose(Boolean(verbose)); @@ -262,8 +267,9 @@ program .description('Generate the snippets') .addArgument(args.language) .addArgument(args.clients) - .option(flags.verbose.flag, flags.verbose.description) - .action(async (langArg: LangArg, clientArg: string[], { verbose }) => { + .addOption(flags.verbose) + .addOption(flags.debugger) + .action(async (langArg: LangArg, clientArg: string[], { verbose, debugger: withDebugger }) => { const { language, client, clientList } = transformSelection({ langArg, clientArg, @@ -271,7 +277,7 @@ program setVerbose(Boolean(verbose)); - await docsGenerateMany(generatorList({ language, client, clientList }), 'snippets'); + await docsGenerateMany(generatorList({ language, client, clientList }), 'snippets', Boolean(withDebugger)); }); program @@ -279,8 +285,9 @@ program .description('Generate the guides') .addArgument(args.language) .addArgument(args.clients) - .option(flags.verbose.flag, flags.verbose.description) - .action(async (langArg: LangArg, clientArg: string[], { verbose }) => { + .addOption(flags.verbose) + .addOption(flags.debugger) + .action(async (langArg: LangArg, clientArg: string[], { verbose, debugger: withDebugger }) => { const { language, client, clientList } = transformSelection({ langArg, clientArg, @@ -293,13 +300,14 @@ program existsSync(toAbsolutePath(`templates/${gen.language}/guides/${gen.client}`)), ), 'guides', + Boolean(withDebugger), ); }); program .command('release') .description('Releases the client') - .option(flags.verbose.flag, flags.verbose.description) + .addOption(flags.verbose) .option( '--rt --release-type ', 'triggers a release for the given language list with the given releaseType', diff --git a/scripts/common.ts b/scripts/common.ts index 58cc7e20b41..beb2fe3dafb 100644 --- a/scripts/common.ts +++ b/scripts/common.ts @@ -2,6 +2,7 @@ import fsp from 'fs/promises'; import path from 'path'; import { Octokit } from '@octokit/rest'; +import chalk from 'chalk'; import type { ExecaError } from 'execa'; import { execa, execaCommand } from 'execa'; import { remove } from 'fs-extra'; @@ -270,13 +271,40 @@ export function isVerbose(): boolean { return verbose; } -export async function callGenerator(gen: Generator): Promise { - await run( - // Use the following line if you want to be able to attach a debugger to the generators - // `JAVA_OPTS="-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=\*:5009" yarn openapi-generator-cli --custom-generator=generators/build/libs/algolia-java-openapi-generator-1.0.0.jar generate --generator-key ${gen.key}`, - `yarn openapi-generator-cli --custom-generator=generators/build/libs/algolia-java-openapi-generator-1.0.0.jar generate --generator-key ${gen.key}`, - { language: 'java' }, - ); +export async function callGenerator(gen: Generator, withDebugger: boolean): Promise { + const cmd = `yarn openapi-generator-cli --custom-generator=generators/build/libs/algolia-java-openapi-generator-1.0.0.jar generate --generator-key ${gen.key}`; + if (!withDebugger) { + await run(cmd, { language: 'java' }); + return; + } + + console.log(chalk.yellow('Running the generator in debug mode, waiting for debugger to be attached on port 5009')); + + /* + example .vscode/launch.json config to attach the debugger + { + "version": "0.2.0", + "configurations": [ + { + "type": "java", + "name": "APIC Generator", + "request": "attach", + "hostName": "localhost", + "port": "5009" + } + ] + } + */ + + const verbose = isVerbose(); + setVerbose(false); // verbose messes up the order of execution + + // kill previous debuggers + await run('(killall -9 java && sleep 1) || true', { language: 'java' }); + setVerbose(verbose); + await run(`JAVA_OPTS="-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=*:5009" ${cmd}`, { + language: 'java', + }); } export function isWSL(): boolean { diff --git a/scripts/cts/generate.ts b/scripts/cts/generate.ts index 5eba3fa8370..fa35cf578c6 100644 --- a/scripts/cts/generate.ts +++ b/scripts/cts/generate.ts @@ -3,13 +3,17 @@ import { getTestOutputFolder } from '../config.ts'; import { formatter } from '../formatter.ts'; import type { Generator } from '../types.ts'; -export async function ctsGenerateMany(generators: Generator[], languageVersion = ''): Promise { +export async function ctsGenerateMany( + generators: Generator[], + withDebugger: boolean, + languageVersion = '', +): Promise { await setupAndGen( generators, 'tests', async (gen) => { if (getTestOutputFolder(gen.language)) { - await callGenerator(gen); + await callGenerator(gen, withDebugger); } }, { diff --git a/scripts/docker/Dockerfile.base b/scripts/docker/Dockerfile.base index a97c146f054..efbabe6a32b 100644 --- a/scripts/docker/Dockerfile.base +++ b/scripts/docker/Dockerfile.base @@ -16,6 +16,8 @@ SHELL ["/bin/bash", "--login", "-c"] # Global dependencies RUN apt-get update \ && apt-get install -y --no-install-recommends git curl zip unzip libexpat1-dev libicu76 \ + # for killall and ps + && apt-get install -y procps \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* diff --git a/scripts/docs/generate.ts b/scripts/docs/generate.ts index 00849c61fb5..0cba87246c1 100644 --- a/scripts/docs/generate.ts +++ b/scripts/docs/generate.ts @@ -3,10 +3,14 @@ import { getTestOutputFolder } from '../config.ts'; import { formatter } from '../formatter.ts'; import type { Generator } from '../types.ts'; -export async function docsGenerateMany(generators: Generator[], scope: 'guides' | 'snippets'): Promise { +export async function docsGenerateMany( + generators: Generator[], + scope: 'guides' | 'snippets', + withDebugger: boolean, +): Promise { await setupAndGen(generators, scope, async (gen) => { if (getTestOutputFolder(gen.language)) { - await callGenerator(gen); + await callGenerator(gen, withDebugger); } }); diff --git a/scripts/generate.ts b/scripts/generate.ts index e2f6eae3054..b2442a48713 100644 --- a/scripts/generate.ts +++ b/scripts/generate.ts @@ -8,10 +8,10 @@ async function preGen(gen: Generator): Promise { await removeExistingCodegen(gen); } -export async function generate(generators: Generator[]): Promise { +export async function generate(generators: Generator[], withDebugger: boolean): Promise { await setupAndGen(generators, 'client', async (gen) => { await preGen(gen); - await callGenerator(gen); + await callGenerator(gen, withDebugger); }); for (const lang of new Set(generators.map((gen) => gen.language))) { From 40dd83b200eb769a8f5d545fade6df626d165240 Mon Sep 17 00:00:00 2001 From: Pierre Millot Date: Wed, 17 Sep 2025 15:13:52 +0200 Subject: [PATCH 2/4] remove dubug log --- scripts/cli/index.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/scripts/cli/index.ts b/scripts/cli/index.ts index 75cf1e6c76e..3d163548939 100644 --- a/scripts/cli/index.ts +++ b/scripts/cli/index.ts @@ -60,8 +60,6 @@ program setVerbose(Boolean(verbose)); - console.log({ withDebugger }); - await generate(generatorList({ language, client, clientList }), Boolean(withDebugger)); }); From 45aacfd79ebb6b7693c13f4977c577b08de7e02e Mon Sep 17 00:00:00 2001 From: Pierre Millot Date: Wed, 17 Sep 2025 15:37:48 +0200 Subject: [PATCH 3/4] doc --- website/docs/CLI/cts-commands.md | 33 +++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/website/docs/CLI/cts-commands.md b/website/docs/CLI/cts-commands.md index 3ac58758dee..748b23063da 100644 --- a/website/docs/CLI/cts-commands.md +++ b/website/docs/CLI/cts-commands.md @@ -26,9 +26,10 @@ apic cts generate ### Available options -| Option | Command | Description | -| ------- | :------------ | :------------------------------------------------------------ | -| verbose | -v, --verbose | Make the process verbose, display logs from third party tools | +| Option | Command | Description | +|----------|:---------------|:----------------------------------------------------------------------------------| +| verbose | -v, --verbose | Make the process verbose, display logs from third party tools | +| debugger | -d, --debugger | runs the generator in debug mode, it will wait for a Java debugger to be attached | ## Generate @@ -50,6 +51,32 @@ apic cts generate java search apic cts generate php insights recommend search ``` +### Attach a debugger to the generator + +Using VS code extension [Debugger For Java](https://code.visualstudio.com/docs/java/java-debugging), you can attach breakpoint to the generator. +Example config for VS Code in `.vscode/launch.json`: + +```json +{ + "version": "0.2.0", + "configurations": [ + { + "type": "java", + "name": "APIC Generator", + "request": "attach", + "hostName": "localhost", + "port": "5009" + } + ] +} +``` + +```bash +apic cts generate java search -d +``` + +Then you can connect you favorite Java debugger, either IntelliJ, VS Code, or jdb. + ## Run ### Run CTS for all supported languages From f5f28c3b95ec72b1833d3fdfb3c9d530e7255eb5 Mon Sep 17 00:00:00 2001 From: Pierre Millot Date: Wed, 17 Sep 2025 16:09:49 +0200 Subject: [PATCH 4/4] link to the doc --- scripts/common.ts | 22 +++++----------------- website/docs/CLI/generate-commands.md | 7 ++++--- 2 files changed, 9 insertions(+), 20 deletions(-) diff --git a/scripts/common.ts b/scripts/common.ts index beb2fe3dafb..8678540e416 100644 --- a/scripts/common.ts +++ b/scripts/common.ts @@ -278,23 +278,11 @@ export async function callGenerator(gen: Generator, withDebugger: boolean): Prom return; } - console.log(chalk.yellow('Running the generator in debug mode, waiting for debugger to be attached on port 5009')); - - /* - example .vscode/launch.json config to attach the debugger - { - "version": "0.2.0", - "configurations": [ - { - "type": "java", - "name": "APIC Generator", - "request": "attach", - "hostName": "localhost", - "port": "5009" - } - ] - } - */ + console.log( + chalk.yellow( + 'Running the generator in debug mode, waiting for debugger to be attached on port 5009\nsee the doc for reference: https://api-clients-automation.netlify.app/docs/CLI/cts-commands#attach-a-debugger-to-the-generator', + ), + ); const verbose = isVerbose(); setVerbose(false); // verbose messes up the order of execution diff --git a/website/docs/CLI/generate-commands.md b/website/docs/CLI/generate-commands.md index 0327fbc1d69..e7608e53867 100644 --- a/website/docs/CLI/generate-commands.md +++ b/website/docs/CLI/generate-commands.md @@ -24,9 +24,10 @@ apic generate ### Available options -| Option | Command | Description | -| ------- | :------------ | :------------------------------------------------------------ | -| verbose | -v, --verbose | Make the process verbose, display logs from third party tools | +| Option | Command | Description | +|----------|:---------------|:----------------------------------------------------------------------------------| +| verbose | -v, --verbose | Make the process verbose, display logs from third party tools | +| debugger | -d, --debugger | runs the generator in debug mode, it will wait for a Java debugger to be attached | ## Generate