Skip to content

Implementation: v4 codegen support in apimatic sdk publish #311

Description

@MuHamza30

Add --codegen-version to apimatic sdk publish

Summary

apimatic sdk generate supports both the v3 and v4 code generators via --codegen-version. apimatic sdk publish does not — it hardcodes v3. This adds the same flag to publish so a v4 SDK can be generated and published.

The change is a flag plus parameter threading through four files, plus one stability fix the v4 path turned out to require. No service changes, no prompt changes, no changes to GenerateAction.

Background — why this is small

The v4 generation path already exists and works. GenerateAction (src/actions/sdk/generate.ts:109) branches on codeGenVersion and calls PortalService.generateV4Sdk. The only reason sdk publish can't reach it is that SdkPublishAction passes hardcoded literals:

// src/actions/sdk/publish.ts (before)
CodeGenerationVersion.V3,
Stability.STABLE,

It's also worth stating why publishing needs no changes at all. The CLI talks to two independent services:

Stage Service Endpoint
Generation api.apimatic.io POST /sdk (v3) or POST /sdk/v2 (v4)
Publishing api.package-publishing.apimatic.io POST /publish/{profileId}/{language}

The CLI generates the SDK itself, then uploads the finished zip to the publishing service (src/infrastructure/services/publishing-api-service.ts:46-78) along with packageVersion and publishType. The publishing service never learns which generator produced the zip.

This also settles the package-version question: --version is sent to the publishing service as an explicit form field (publishing-api-service.ts:67), and that is the service that performs the registry push. The v3 generator additionally receives it (portal-service.ts:117) while the v4 generator has no such parameter, but that's redundant rather than load-bearing.

Stability is not independent of codegen version

The original scoping treated --stability as an unrelated knob and excluded it. That was wrong, and a live run proved it:

The V4 Code Generator currently supports SDK generation for 'csharp' only in 'beta'.

Choosing v4 constrains which stability values the server accepts. Because SdkPublishAction pinned Stability.STABLE, --codegen-version v4 could never generate anything at all.

Note also that stability only exists on the v4 path — portal-service.ts:111 (generateSdk, v3) takes no stability parameter, while portal-service.ts:172 (generateV4Sdk) requires one. So this is inert for v3.

Resolution: the v4 path asks for beta; v3 keeps stable. No new flag.

A --stability flag was considered and rejected. v4 is csharp-only in practice, this is a temporary server-side restriction, and a flag would force users to pass two flags that always go together. The conditional is keyed on v4 rather than on csharp, so if another language joins v4 later it also starts in beta without anyone editing a language check.

Out of scope

  • A --stability flag — see above; the v4 path selects beta automatically.
  • An interactive prompt for codegen version — see below.
  • A guard for builds with tracked SDK customizations — see Known limitations.
  • Anything touching --version / SemVersion — unrelated.

Interactive mode needs no special handling

src/commands/sdk/publish.ts:86 decides the mode with:

const interactive = this.argv.length === 0;

Interactive mode means no flags were passed at all, so codegenVersion is always its default (v3) on that path. Threading the parsed value through both branches uniformly is correct and requires no hardcoding or prompting.

Consequence: the wizard cannot produce a v4 SDK. This is deliberate for now — v4 publishing targets CI/CD and scripted use. If interactive v4 is wanted later, it is additive: the parameter is already threaded, so only the mode check changes (drop --codegen-version from the argv count, handling both --codegen-version v4 and --codegen-version=v4). Nothing in this change would be undone.

Passing --codegen-version v4 alone routes to the non-interactive path, which reports the missing required flags and prints interactiveModeNotice()"You can run the command in interactive mode by not passing any flags" (src/prompts/sdk/publish/non-interactive.ts:32-34). Same as passing any single flag today.


Change 1 — src/commands/sdk/publish.ts

1a. Import

// before
import { Language } from '../../types/sdk/generate.js';

// after
import { CodeGenerationVersion, Language } from '../../types/sdk/generate.js';

1b. Flag

Add as the last entry in static flags, after 'dry-run':

    'dry-run': Flags.boolean({
      default: false,
      description: 'Generate the SDK locally for review without publishing.'
    }),
    'codegen-version': Flags.string({
      description: 'Version of the code generator to use',
      options: Object.values(CodeGenerationVersion).map((v) => v.valueOf()),
      default: CodeGenerationVersion.V3
    })

Name, description, options and default are copied verbatim from src/commands/sdk/generate.ts:46-50 so the two commands stay consistent.

1c. Example

Add one v4 example to static examples:

    `${SdkPublish.cmdTxt} ${format.flag('profile-id', 'd4e5f6a1b2c3d4e5f6a1b2c3')} ${format.flag(
      'language',
      'typescript'
    )} ${format.flag('version', '1.0.0')} ${format.flag(
      'publish-type',
      PublishType.PackagePublishing
    )} ${format.flag('codegen-version', 'v4')}`

test/commands/examples-parse.test.ts parses every command's examples against its own flag definitions, so this is validated automatically.

1d. Destructure

    const {
      flags: {
        'profile-id': profileId,
        version,
        destination,
        language,
        force,
        input,
        'publish-type': publishType,
        'dry-run': dryRun,
        'codegen-version': codegenVersion
      }
    } = await this.parse(SdkPublish);

1e. Telemetry payload

Add the new flag to the non-interactive failure event so a failed v4 publish is distinguishable from a failed v3 one:

            new SdkPublishValidationFailedEvent(errorMessage, SdkPublish.id, {
              'profile-id': profileId,
              version,
              language,
              ...(force && { force }),
              'publish-type': publishTypes,
              'codegen-version': codegenVersion
            }),

1f. Pass to both actions

    intro('Publish SDK');
    const result = interactive
      ? await new SdkPublishInteractiveAction(configDir, commandMetadata).execute(
          workingDirectory,
          codegenVersion as CodeGenerationVersion,
          onPublishSdkError
        )
      : await new SdkPublishNonInteractiveAction(configDir, commandMetadata).execute(
          buildDirectory,
          sdkDirectory,
          language as Language,
          publishTypes,
          force,
          dryRun,
          codegenVersion as CodeGenerationVersion,
          onPublishSdkError,
          profileId,
          version
        );
    outro(result);

The as CodeGenerationVersion cast matches how language as Language is already handled here and how sdk generate does it.


Change 2 — src/actions/sdk/publish/non-interactive.ts

2a. Import

// before
import { Language } from '../../../types/sdk/generate.js';

// after
import { CodeGenerationVersion, Language } from '../../../types/sdk/generate.js';

2b. Signature

codegenVersion goes after dryRun, keeping all required parameters ahead of the callback and the trailing optionals:

  public readonly execute = async (
    buildDirectory: DirectoryPath,
    sdkDirectory: DirectoryPath,
    language: Language,
    publishTypes: PublishType[],
    force: boolean,
    dryRun: boolean,
    codegenVersion: CodeGenerationVersion,
    onPublishSdkError: (errorMessage: string) => void,
    profileId?: string,
    version?: string
  ): Promise<ActionResult> => {

2c. Forward to SdkPublishAction

    const publishResult = await new SdkPublishAction(this.configDir, this.commandMetadata).execute(
      buildDirectory,
      outputDir,
      language,
      publishTypes,
      force,
      publishingProfileId,
      semVersion,
      publishingProfile,
      dryRun,
      codegenVersion,
      onPublishSdkError
    );

Everything above this line — directory checks, missing-flag collection, version and profile-id validation, profile lookup, language and publish-type checks — is unchanged.


Change 3 — src/actions/sdk/publish/interactive.ts

3a. Import

This file has no import from types/sdk/generate.js yet; add one:

import { CodeGenerationVersion } from '../../../types/sdk/generate.js';

3b. Signature

  public readonly execute = async (
    defaultBuildDirectory: DirectoryPath,
    codegenVersion: CodeGenerationVersion,
    onPublishSdkError: (errorMessage: string) => void
  ): Promise<ActionResult> => {

3c. Forward to SdkPublishAction

    const publishResult = await new SdkPublishAction(this.configDir, this.commandMetadata).execute(
      buildDirectory,
      sdkDirectory,
      language,
      publishTypes,
      false,
      publishingProfileId,
      version,
      publishingProfile,
      false,
      codegenVersion,
      onPublishSdkError
    );

No new prompt, no new wizard step. The seven existing steps are untouched.


Change 4 — src/actions/sdk/publish.ts

CodeGenerationVersion and Stability are already imported here (line 11), so no import change.

4a. Signature

  public readonly execute = async (
    buildDirectory: DirectoryPath,
    outputDirectory: DirectoryPath,
    language: Language,
    publishType: PublishType[],
    force: boolean,
    profileId: ProfileId,
    semVersion: SemVersion,
    publishingProfile: PublishingProfile,
    dryRun: boolean,
    codegenVersion: CodeGenerationVersion,
    onPublishSdkError: (errorMessage: string) => void
  ): Promise<ActionResult<PublishingInfo>> => {

4b. Use it — the actual fix

Both hardcoded literals go. The codegen version becomes the caller's, and stability follows from it:

      const sdkGenerateAction = new GenerateAction(this.configDir, this.commandMetadata);
      const sdkGenerationResult = await sdkGenerateAction.execute(
        buildDirectory,
        outputDirectory,
        language,
        force,
        false,
        false,
        false,
        codegenVersion,
        // The V4 generator is only offered in beta today, so asking for a stable V4 SDK is
        // rejected server-side. Drop this once V4 reaches general availability.
        codegenVersion === CodeGenerationVersion.V4 ? Stability.BETA : Stability.STABLE,
        undefined,
        semVersion,
        packageSettingsDirectory
      );

Package-settings writing, the dry-run branch, zipping, the publish call and the error path are all untouched.


Verified against the live API

A real run against api.apimatic.io confirmed the full CLI path:

◇  Profile search complete.
▲  The V4 Code Generator does not currently support SDK customizations.   ← v4 branch taken
◇  SDK generated successfully.                                            ← beta stability accepted
●  The generated SDK can be found at '...\sdk\csharp'.
◇  Publishing initiated.                                                  ← zip uploaded

The warning on line 2 exists only inside GenerateAction's v4 branch (generate.ts:110), so its presence in a publish run proves the value threaded through all four layers and reached POST /sdk/v2. Before the stability fix, the same run failed at generation with the beta error quoted above.


Known limitations

1. v4 and tracked SDK customizations. On the v4 path, GenerateAction prints sdkCustomizationsNotSupportedForV4() and returns at generate.ts:131, before the MergeSourceTreeAction block at line 167. A build containing sdk-source-tree/.{language} therefore generates — and now publishes — an SDK without those changes, with only a warning line as the signal. Pre-existing property of the v4 path, accepted here. If we later guard it, the check must run against the versioned build context (generate.ts:98), not the root one.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions