diff --git a/e2e/nx-plugin-e2e/tests/executor-cli.e2e.test.ts b/e2e/nx-plugin-e2e/tests/executor-cli.e2e.test.ts
index 2a3797492..e5561a462 100644
--- a/e2e/nx-plugin-e2e/tests/executor-cli.e2e.test.ts
+++ b/e2e/nx-plugin-e2e/tests/executor-cli.e2e.test.ts
@@ -100,6 +100,32 @@ describe('executor command', () => {
     ).rejects.toThrow('');
   });
 
+  it('should execute print-config executor with api key', async () => {
+    const cwd = path.join(testFileDir, 'execute-print-config-command');
+    await addTargetToWorkspace(tree, { cwd, project });
+
+    const { stdout, code } = await executeProcess({
+      command: 'npx',
+      args: [
+        'nx',
+        'run',
+        `${project}:code-pushup`,
+        'print-config',
+        '--upload.apiKey=a123a',
+      ],
+      cwd,
+    });
+
+    expect(code).toBe(0);
+    const cleanStdout = removeColorCodes(stdout);
+    expect(cleanStdout).toContain('nx run my-lib:code-pushup print-config');
+    expect(cleanStdout).toContain('a123a');
+
+    await expect(() =>
+      readJsonFile(path.join(cwd, '.code-pushup', project, 'report.json')),
+    ).rejects.toThrow('');
+  });
+
   it('should execute collect executor and merge target and command-line options', async () => {
     const cwd = path.join(testFileDir, 'execute-collect-with-merged-options');
     await addTargetToWorkspace(
diff --git a/packages/models/code-pushup.config.ts b/packages/models/code-pushup.config.ts
new file mode 100644
index 000000000..dad38404f
--- /dev/null
+++ b/packages/models/code-pushup.config.ts
@@ -0,0 +1,10 @@
+import { eslintCoreConfigNx } from '../../code-pushup.preset.js';
+import { mergeConfigs } from '../../dist/packages/utils/src/index.js';
+
+// see: https://github.com/code-pushup/cli/blob/main/packages/models/docs/models-reference.md#coreconfig
+export default mergeConfigs(
+  {
+    plugins: [],
+  },
+  await eslintCoreConfigNx(),
+);
diff --git a/packages/models/tsconfig.lib.json b/packages/models/tsconfig.lib.json
index 6b178086a..ae5510ec7 100644
--- a/packages/models/tsconfig.lib.json
+++ b/packages/models/tsconfig.lib.json
@@ -15,6 +15,7 @@
   "exclude": [
     "vite.config.unit.ts",
     "vite.config.integration.ts",
+    "code-pushup.config.ts",
     "zod2md.config.ts",
     "src/**/*.test.ts",
     "src/**/*.mock.ts",
diff --git a/packages/models/tsconfig.test.json b/packages/models/tsconfig.test.json
index bb1ab5e0c..c7a178bbe 100644
--- a/packages/models/tsconfig.test.json
+++ b/packages/models/tsconfig.test.json
@@ -4,6 +4,7 @@
     "outDir": "../../dist/out-tsc",
     "types": ["vitest/globals", "vitest/importMeta", "vite/client", "node"]
   },
+  "exclude": ["**/code-pushup.config.ts"],
   "include": [
     "vite.config.unit.ts",
     "vite.config.integration.ts",
diff --git a/packages/nx-plugin/eslint.config.cjs b/packages/nx-plugin/eslint.config.js
similarity index 96%
rename from packages/nx-plugin/eslint.config.cjs
rename to packages/nx-plugin/eslint.config.js
index e732748e6..937c98765 100644
--- a/packages/nx-plugin/eslint.config.cjs
+++ b/packages/nx-plugin/eslint.config.js
@@ -17,6 +17,7 @@ module.exports = tseslint.config(
     rules: {
       // Nx plugins don't yet support ESM: https://github.com/nrwl/nx/issues/15682
       'unicorn/prefer-module': 'off',
+      'n/file-extension-in-import': 'off',
       // used instead of verbatimModuleSyntax tsconfig flag (requires ESM)
       '@typescript-eslint/consistent-type-imports': [
         'warn',
diff --git a/packages/nx-plugin/src/executors/cli/executor.unit.test.ts b/packages/nx-plugin/src/executors/cli/executor.unit.test.ts
deleted file mode 100644
index 77a81a6f0..000000000
--- a/packages/nx-plugin/src/executors/cli/executor.unit.test.ts
+++ /dev/null
@@ -1,109 +0,0 @@
-import { logger } from '@nx/devkit';
-import { execSync } from 'node:child_process';
-import { afterEach, expect, vi } from 'vitest';
-import { executorContext } from '@code-pushup/test-nx-utils';
-import { MEMFS_VOLUME } from '@code-pushup/test-utils';
-import runAutorunExecutor from './executor.js';
-
-vi.mock('node:child_process', async () => {
-  const actual = await vi.importActual('node:child_process');
-
-  return {
-    ...actual,
-    execSync: vi.fn((command: string) => {
-      if (command.includes('THROW_ERROR')) {
-        throw new Error(command);
-      }
-    }),
-  };
-});
-
-describe('runAutorunExecutor', () => {
-  const loggerInfoSpy = vi.spyOn(logger, 'info');
-  const loggerWarnSpy = vi.spyOn(logger, 'warn');
-
-  afterEach(() => {
-    loggerWarnSpy.mockReset();
-    loggerInfoSpy.mockReset();
-  });
-
-  it('should call execSync with return result', async () => {
-    const output = await runAutorunExecutor({}, executorContext('utils'));
-    expect(output.success).toBe(true);
-    expect(output.command).toMatch('npx @code-pushup/cli');
-    // eslint-disable-next-line n/no-sync
-    expect(execSync).toHaveBeenCalledWith(
-      expect.stringContaining('npx @code-pushup/cli'),
-      { cwd: MEMFS_VOLUME },
-    );
-  });
-
-  it('should normalize context', async () => {
-    const output = await runAutorunExecutor(
-      {},
-      {
-        ...executorContext('utils'),
-        cwd: 'cwd-form-context',
-      },
-    );
-    expect(output.success).toBe(true);
-    expect(output.command).toMatch('utils');
-    // eslint-disable-next-line n/no-sync
-    expect(execSync).toHaveBeenCalledWith(expect.stringContaining('utils'), {
-      cwd: 'cwd-form-context',
-    });
-  });
-
-  it('should process executorOptions', async () => {
-    const output = await runAutorunExecutor(
-      { persist: { filename: 'REPORT' } },
-      executorContext('testing-utils'),
-    );
-    expect(output.success).toBe(true);
-    expect(output.command).toMatch('--persist.filename="REPORT"');
-  });
-
-  it('should create command from context, options and arguments', async () => {
-    vi.stubEnv('CP_PROJECT', 'CLI');
-    const output = await runAutorunExecutor(
-      { persist: { filename: 'REPORT', format: ['md', 'json'] } },
-      executorContext('core'),
-    );
-    expect(output.command).toMatch('--persist.filename="REPORT"');
-    expect(output.command).toMatch(
-      '--persist.format="md" --persist.format="json"',
-    );
-    expect(output.command).toMatch('--upload.project="CLI"');
-  });
-
-  it('should log information if verbose is set', async () => {
-    const output = await runAutorunExecutor(
-      { verbose: true },
-      { ...executorContext('github-action'), cwd: '<CWD>' },
-    );
-    // eslint-disable-next-line n/no-sync
-    expect(execSync).toHaveBeenCalledTimes(1);
-
-    expect(output.command).toMatch('--verbose');
-    expect(loggerWarnSpy).toHaveBeenCalledTimes(0);
-    expect(loggerInfoSpy).toHaveBeenCalledTimes(2);
-    expect(loggerInfoSpy).toHaveBeenCalledWith(
-      expect.stringContaining(`Run CLI executor`),
-    );
-    expect(loggerInfoSpy).toHaveBeenCalledWith(
-      expect.stringContaining('Command: npx @code-pushup/cli'),
-    );
-  });
-
-  it('should log command if dryRun is set', async () => {
-    await runAutorunExecutor({ dryRun: true }, executorContext('utils'));
-
-    expect(loggerInfoSpy).toHaveBeenCalledTimes(0);
-    expect(loggerWarnSpy).toHaveBeenCalledTimes(1);
-    expect(loggerWarnSpy).toHaveBeenCalledWith(
-      expect.stringContaining(
-        'DryRun execution of: npx @code-pushup/cli --dryRun',
-      ),
-    );
-  });
-});
diff --git a/packages/nx-plugin/src/executors/cli/utils.ts b/packages/nx-plugin/src/executors/cli/utils.ts
index 61fccf0e9..afcca4542 100644
--- a/packages/nx-plugin/src/executors/cli/utils.ts
+++ b/packages/nx-plugin/src/executors/cli/utils.ts
@@ -27,6 +27,11 @@ export function parseAutorunExecutorOptions(
   const { projectPrefix, persist, upload, command } = options;
   const needsUploadParams =
     command === 'upload' || command === 'autorun' || command === undefined;
+  const uploadCfg = uploadConfig(
+    { projectPrefix, ...upload },
+    normalizedContext,
+  );
+  const hasApiToken = uploadCfg?.apiKey != null;
   return {
     ...parseAutorunExecutorOnlyOptions(options),
     ...globalConfig(options, normalizedContext),
@@ -34,9 +39,7 @@ export function parseAutorunExecutorOptions(
     // @TODO This is a hack to avoid validation errors of upload config for commands that dont need it.
     // Fix: use utils and execute the core logic directly
     // Blocked by Nx plugins can't compile to es6
-    upload: needsUploadParams
-      ? uploadConfig({ projectPrefix, ...upload }, normalizedContext)
-      : undefined,
+    ...(needsUploadParams && hasApiToken ? { upload: uploadCfg } : {}),
   };
 }
 
diff --git a/packages/nx-plugin/src/executors/cli/utils.unit.test.ts b/packages/nx-plugin/src/executors/cli/utils.unit.test.ts
index 19da42b17..7a4141eff 100644
--- a/packages/nx-plugin/src/executors/cli/utils.unit.test.ts
+++ b/packages/nx-plugin/src/executors/cli/utils.unit.test.ts
@@ -73,14 +73,13 @@ describe('parseAutorunExecutorOptions', () => {
         },
       },
     );
-    expect(osAgnosticPath(executorOptions.config)).toBe(
+    expect(osAgnosticPath(executorOptions.config ?? '')).toBe(
       osAgnosticPath('root/code-pushup.config.ts'),
     );
     expect(executorOptions).toEqual(
       expect.objectContaining({
         progress: false,
         verbose: false,
-        upload: { project: projectName },
       }),
     );
 
@@ -92,20 +91,20 @@ describe('parseAutorunExecutorOptions', () => {
       }),
     );
 
-    expect(osAgnosticPath(executorOptions.persist?.outputDir)).toBe(
+    expect(osAgnosticPath(executorOptions.persist?.outputDir ?? '')).toBe(
       osAgnosticPath('workspaceRoot/.code-pushup/my-app'),
     );
   });
 
   it.each<Command | undefined>(['upload', 'autorun', undefined])(
-    'should include upload config for command %s',
+    'should include upload config for command %s if API key is provided',
     command => {
       const projectName = 'my-app';
       const executorOptions = parseAutorunExecutorOptions(
         {
           command,
           upload: {
-            organization: 'code-pushup',
+            apiKey: '123456789',
           },
         },
         {
diff --git a/packages/nx-plugin/src/index.ts b/packages/nx-plugin/src/index.ts
index 20ebba48a..e516b18ce 100644
--- a/packages/nx-plugin/src/index.ts
+++ b/packages/nx-plugin/src/index.ts
@@ -3,16 +3,16 @@ import { createNodes } from './plugin/index.js';
 // default export for nx.json#plugins
 export default createNodes;
 
-export * from './internal/versions.js';
-export { type InitGeneratorSchema } from './generators/init/schema.js';
-export { initGenerator, initSchematic } from './generators/init/generator.js';
-export type { ConfigurationGeneratorOptions } from './generators/configuration/schema.js';
-export { configurationGenerator } from './generators/configuration/generator.js';
+export type { AutorunCommandExecutorOptions } from './executors/cli/schema.js';
+export { objectToCliArgs } from './executors/internal/cli.js';
 export { generateCodePushupConfig } from './generators/configuration/code-pushup-config.js';
-export { createNodes } from './plugin/index.js';
+export { configurationGenerator } from './generators/configuration/generator.js';
+export type { ConfigurationGeneratorOptions } from './generators/configuration/schema.js';
+export { initGenerator, initSchematic } from './generators/init/generator.js';
+export { type InitGeneratorSchema } from './generators/init/schema.js';
 export {
   executeProcess,
   type ProcessConfig,
 } from './internal/execute-process.js';
-export { objectToCliArgs } from './executors/internal/cli.js';
-export type { AutorunCommandExecutorOptions } from './executors/cli/schema.js';
+export * from './internal/versions.js';
+export { createNodes } from './plugin/index.js';
diff --git a/packages/nx-plugin/src/internal/constants.ts b/packages/nx-plugin/src/internal/constants.ts
index cac41a656..f69356ea3 100644
--- a/packages/nx-plugin/src/internal/constants.ts
+++ b/packages/nx-plugin/src/internal/constants.ts
@@ -1,5 +1,4 @@
-import { name } from '../../package.json';
-
 export const PROJECT_JSON_FILE_NAME = 'project.json';
-export const PACKAGE_NAME = name;
+export const CODE_PUSHUP_CONFIG_REGEX = /^code-pushup(?:\.[\w-]+)?\.ts$/;
+export const PACKAGE_NAME = '@code-pushup/nx-plugin';
 export const DEFAULT_TARGET_NAME = 'code-pushup';
diff --git a/packages/nx-plugin/src/internal/execute-process.ts b/packages/nx-plugin/src/internal/execute-process.ts
index ee5813397..cf61f3e84 100644
--- a/packages/nx-plugin/src/internal/execute-process.ts
+++ b/packages/nx-plugin/src/internal/execute-process.ts
@@ -125,7 +125,7 @@ export type ProcessObserver = {
  * // async process execution
  * const result = await executeProcess({
  *    command: 'node',
- *    args: ['download-data.js'],
+ *    args: ['download-data'],
  *    observer: {
  *      onStdout: updateProgress,
  *      error: handleError,
diff --git a/packages/nx-plugin/src/internal/versions.ts b/packages/nx-plugin/src/internal/versions.ts
index 884dad9a7..b7e24f64a 100644
--- a/packages/nx-plugin/src/internal/versions.ts
+++ b/packages/nx-plugin/src/internal/versions.ts
@@ -16,6 +16,10 @@ export const cpCliVersion = loadPackageJson(
   path.join(projectsFolder, 'models'),
 ).version;
 
+/**
+ * Load the package.json file from the given folder path.
+ * @param folderPath
+ */
 function loadPackageJson(folderPath: string): PackageJson {
   return readJsonFile<PackageJson>(path.join(folderPath, 'package.json'));
 }
diff --git a/packages/nx-plugin/src/plugin/plugin.ts b/packages/nx-plugin/src/plugin/plugin.ts
index 9129f1bd7..1f125f5a8 100644
--- a/packages/nx-plugin/src/plugin/plugin.ts
+++ b/packages/nx-plugin/src/plugin/plugin.ts
@@ -8,7 +8,7 @@ import { createTargets } from './target/targets.js';
 import type { CreateNodesOptions } from './types.js';
 import { normalizedCreateNodesContext } from './utils.js';
 
-// name has to be "createNodes" to get picked up by Nx
+// name has to be "createNodes" to get picked up by Nx <v20
 export const createNodes: CreateNodes = [
   `**/${PROJECT_JSON_FILE_NAME}`,
   async (
diff --git a/packages/nx-plugin/src/plugin/plugin.unit.test.ts b/packages/nx-plugin/src/plugin/plugin.unit.test.ts
index c51ebf570..62b3c0b2f 100644
--- a/packages/nx-plugin/src/plugin/plugin.unit.test.ts
+++ b/packages/nx-plugin/src/plugin/plugin.unit.test.ts
@@ -13,6 +13,7 @@ describe('@code-pushup/nx-plugin/plugin', () => {
     context = {
       nxJsonConfiguration: {},
       workspaceRoot: '',
+      configFiles: [],
     };
   });
 
diff --git a/packages/nx-plugin/src/plugin/target/targets.ts b/packages/nx-plugin/src/plugin/target/targets.ts
index 3e659688b..55e608896 100644
--- a/packages/nx-plugin/src/plugin/target/targets.ts
+++ b/packages/nx-plugin/src/plugin/target/targets.ts
@@ -1,13 +1,19 @@
 import { readdir } from 'node:fs/promises';
 import { CP_TARGET_NAME } from '../constants.js';
-import type { NormalizedCreateNodesContext } from '../types.js';
+import type {
+  CreateNodesOptions,
+  ProjectConfigurationWithName,
+} from '../types.js';
 import { createConfigurationTarget } from './configuration-target.js';
 import { CODE_PUSHUP_CONFIG_REGEX } from './constants.js';
 import { createExecutorTarget } from './executor-target.js';
 
-export async function createTargets(
-  normalizedContext: NormalizedCreateNodesContext,
-) {
+export type CreateTargetsOptions = {
+  projectJson: ProjectConfigurationWithName;
+  projectRoot: string;
+  createOptions: CreateNodesOptions;
+};
+export async function createTargets(normalizedContext: CreateTargetsOptions) {
   const {
     targetName = CP_TARGET_NAME,
     bin,
diff --git a/packages/nx-plugin/src/plugin/types.ts b/packages/nx-plugin/src/plugin/types.ts
index 5e8d59db7..4fd57ed95 100644
--- a/packages/nx-plugin/src/plugin/types.ts
+++ b/packages/nx-plugin/src/plugin/types.ts
@@ -1,6 +1,11 @@
-import type { CreateNodesContext, ProjectConfiguration } from '@nx/devkit';
+import type {
+  CreateNodesContext,
+  CreateNodesContextV2,
+  ProjectConfiguration,
+} from '@nx/devkit';
 import type { WithRequired } from '@code-pushup/utils';
 import type { DynamicTargetOptions } from '../internal/types.js';
+import type { CreateTargetsOptions } from './target/targets.js';
 
 export type ProjectPrefixOptions = {
   projectPrefix?: string;
@@ -13,8 +18,8 @@ export type ProjectConfigurationWithName = WithRequired<
   'name'
 >;
 
-export type NormalizedCreateNodesContext = CreateNodesContext & {
-  projectJson: ProjectConfigurationWithName;
-  projectRoot: string;
-  createOptions: CreateNodesOptions;
-};
+export type NormalizedCreateNodesContext = CreateNodesContext &
+  CreateTargetsOptions;
+
+export type NormalizedCreateNodesV2Context = CreateNodesContextV2 &
+  CreateTargetsOptions;
diff --git a/packages/nx-plugin/src/plugin/utils.ts b/packages/nx-plugin/src/plugin/utils.ts
index e7a819f8d..8d551f682 100644
--- a/packages/nx-plugin/src/plugin/utils.ts
+++ b/packages/nx-plugin/src/plugin/utils.ts
@@ -1,10 +1,11 @@
-import type { CreateNodesContext } from '@nx/devkit';
+import type { CreateNodesContext, CreateNodesContextV2 } from '@nx/devkit';
 import { readFile } from 'node:fs/promises';
 import * as path from 'node:path';
 import { CP_TARGET_NAME } from './constants.js';
 import type {
   CreateNodesOptions,
   NormalizedCreateNodesContext,
+  NormalizedCreateNodesV2Context,
   ProjectConfigurationWithName,
 } from './types.js';
 
@@ -36,3 +37,29 @@ export async function normalizedCreateNodesContext(
     );
   }
 }
+
+export async function normalizedCreateNodesV2Context(
+  context: CreateNodesContextV2,
+  projectConfigurationFile: string,
+  createOptions: CreateNodesOptions = {},
+): Promise<NormalizedCreateNodesV2Context> {
+  const projectRoot = path.dirname(projectConfigurationFile);
+
+  try {
+    const projectJson = JSON.parse(
+      (await readFile(projectConfigurationFile)).toString(),
+    ) as ProjectConfigurationWithName;
+
+    const { targetName = CP_TARGET_NAME } = createOptions;
+    return {
+      ...context,
+      projectJson,
+      projectRoot,
+      createOptions: { ...createOptions, targetName },
+    };
+  } catch {
+    throw new Error(
+      `Error parsing project.json file ${projectConfigurationFile}.`,
+    );
+  }
+}
diff --git a/testing/test-nx-utils/src/lib/utils/nx-plugin.ts b/testing/test-nx-utils/src/lib/utils/nx-plugin.ts
index 30d9706ba..65676bd9a 100644
--- a/testing/test-nx-utils/src/lib/utils/nx-plugin.ts
+++ b/testing/test-nx-utils/src/lib/utils/nx-plugin.ts
@@ -56,6 +56,21 @@ export async function invokeCreateNodesOnVirtualFiles<
 }
 
 export function createNodesContext(
+  options?: Partial<CreateNodesContext>,
+): CreateNodesContext {
+  const {
+    workspaceRoot = process.cwd(),
+    nxJsonConfiguration = {},
+    configFiles = [],
+  } = options ?? {};
+  return {
+    workspaceRoot,
+    nxJsonConfiguration,
+    configFiles,
+  };
+}
+
+export function createNodesV2Context(
   options?: Partial<CreateNodesContextV2>,
 ): CreateNodesContextV2 {
   const { workspaceRoot = process.cwd(), nxJsonConfiguration = {} } =
diff --git a/testing/test-nx-utils/src/lib/utils/nx-plugin.unit.test.ts b/testing/test-nx-utils/src/lib/utils/nx-plugin.unit.test.ts
index 68a9d5daa..7710cfccf 100644
--- a/testing/test-nx-utils/src/lib/utils/nx-plugin.unit.test.ts
+++ b/testing/test-nx-utils/src/lib/utils/nx-plugin.unit.test.ts
@@ -11,18 +11,22 @@ describe('createNodesContext', () => {
       workspaceRoot: 'root',
       nxJsonConfiguration: { plugins: [] },
     });
-    expect(context).toEqual({
-      workspaceRoot: 'root',
-      nxJsonConfiguration: { plugins: [] },
-    });
+    expect(context).toStrictEqual(
+      expect.objectContaining({
+        workspaceRoot: 'root',
+        nxJsonConfiguration: { plugins: [] },
+      }),
+    );
   });
 
   it('should return a context with defaults', () => {
     const context = createNodesContext();
-    expect(context).toEqual({
-      workspaceRoot: process.cwd(),
-      nxJsonConfiguration: {},
-    });
+    expect(context).toStrictEqual(
+      expect.objectContaining({
+        workspaceRoot: process.cwd(),
+        nxJsonConfiguration: {},
+      }),
+    );
   });
 });