Skip to content

Implementation: v4 codegen support in apimatic sdk publish #311

Description

@MuHamza30

Add --codegen-version to apimatic sdk publish

Blocks #304 (apimatic plugin generate). A4 of #304 writes Languages.<lang>.version into
pluginconfig.json from the codegen version a publish ran with. Until this lands that value can
only be "v3", making v4 plugins unreachable. #304 previously listed this work as a deferred
follow-up; it has been re-scoped as a prerequisite.

Specifically, PR 1 (non-interactive) is the prerequisite — not the whole issue. #304's A4 is
explicitly non-interactive-only, so it never reads a codegen version from the wizard. PR 2 can
proceed in parallel with the rest of #304. See Delivery below.

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, plus
--stability (which generate already has), so a v4 SDK can be generated and published.

SdkPublishAction currently pins both values:

// src/actions/sdk/publish.ts:58-59 (before)
CodeGenerationVersion.V3,
Stability.STABLE,

Both become parameters supplied by the caller. The action itself gains no conditional logic.

Background — the two services

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 the hardcoded literals above.

Publishing itself needs no changes, because 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.

The package version must come from the generator — verified

An earlier draft of this issue assumed --version alone determined the published version, making
the generator's copy redundant. That assumption is wrong. Traced through the
package-publishing and package-publishing-func repositories:

  1. The CLI posts to POST /publish/{profileId}/{language}
    (publishing-api-service.ts:76), which routes to the PublishGeneratedSdk handler
    (PublishingController.cs:20-28) — not the Publish handler on POST /publish/{profileId}.
  2. PublishGeneratedSdk.Handle uploads the CLI's zip verbatim
    await _blobStorageService.UploadBlob(sdkFileStream, publishParams.GenerateBlobName()) — then
    enqueues a metadata message. It never transforms the SDK.
  3. The transforming call, IApimaticCoreService.GeneratePublishReadySdk(...), and the core config
    built from ToCoreConfig(version) are referenced from exactly one place — Publish.cs:210-211,
    the other endpoint, where the service generates the SDK itself from an API entity. The CLI's
    path never reaches it.
  4. The worker packs what it was given. CSharpPublishParams.cs:33-38:
    dotnet pack /sdk/*.Standard/*.Standard.csproj -o out
    dotnet nuget push -s ${PACKAGE_SOURCE} -k ${NUGET_KEY} /out/*.nupkg
    
    No -p:PackageVersion, no package-id override. The generated project file is authoritative.

So the published version and package identity are whatever the generator baked in. --version
reaches the publish log, the Mixpanel event and the constructed PackageUrl
(CSharpConfiguration.cs:23) — but not the package.

v3 is correct today only because the CLI passes the version to the generator
(portal-service.ts:133, the 5th argument to generateSdkViaBuildInputAsync). generateV4Sdk
passes nothing, so a v4 publish would push the generator's default version while every surrounding
record claims the requested one.

Resolution: the package version must be sent to the v4 generation endpoint exactly as it is for
v3. The generated client does not expose the parameter yet
(generateV2SdkViaBuildInputAsync(contentType, file, language, stabilityTag)), so a TODO marks
the call site until the client is regenerated. Threading --version into generateV4Sdk is a
prerequisite for v4 publishing being correct, not merely reachable.


Design decisions

--stability is an explicit flag, never inferred

An earlier draft coerced stability automatically (v4 ? BETA : STABLE) and added no flag. That
was rejected.
The coercion would be baked into a released binary, so when v4 reaches GA every CI
pipeline pinned to an older CLI would keep silently receiving beta SDKs — a user who wrote
--codegen-version v4 months earlier would be opted into unstable output without ever asking for
it. Deleting the ternary at GA only helps people who upgrade.

Instead, publish gains --stability mirroring generate. A user who wants v4 today must write
both flags: --codegen-version v4 --stability beta.

The CLI holds no knowledge of which stability values v4 supports

--codegen-version v4 on its own defaults --stability to STABLE, reaches the server, and comes
back with the service's own error:

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

This is deliberate. Validating locally would hardcode "v4 is beta-only" into a shipped binary, so
on GA day every older CLI would reject a perfectly valid --codegen-version v4 --stability stable
with a false error — and unlike a server error, no server-side fix can reach it. The set of
supported (codegen, stability) pairs belongs to the generation service and changes without a CLI
release.

The cost is that the error arrives after a round trip, in the service's words rather than ours.
Accepted.

--stability is inert on v3 — warn, in both commands

portal-service.ts:111 (generateSdk, v3) takes no stability parameter; only
generateV4Sdk (portal-service.ts:172) does. So --codegen-version v3 --stability beta accepts
the flag, sends nothing, and returns a stable SDK while the user believes otherwise.

Emit a warning whenever --stability is explicitly provided alongside --codegen-version v3,
in both sdk generate and sdk publish. sdk generate has this bug today.

Note this is a different kind of knowledge from the point above: "which values v4 supports" is the
server's and must not be hardcoded; "the v3 code path never sends stability" is the CLI's own
knowledge of its own code, which no server change can invalidate.

Trigger is typed-and-v3, not value-is-beta--stability stable is equally ignored and
equally worth flagging, and beta-specific wording would read as wrong after GA.

Detecting "typed" uses oclif parse metadata, confirmed present in the pinned @oclif/core v4
(node_modules/@oclif/core/lib/interfaces/parser.d.ts:53, MetadataFlag.setFromDefault?: boolean):

const { flags: { ... }, metadata } = await this.parse(SdkPublish);
warnIfStabilityIgnored(codegenVersion, metadata.flags.stability?.setFromDefault !== true);

This is the codebase's first use of parse metadata.

Placement: the command layer, via one shared exported function (src/prompts/sdk/stability.ts).
Not inside GenerateAction, even though both commands converge there — "did the user type this
flag?" is parse metadata available only at the command layer, so warning deeper would mean threading
a presentation-only boolean through four signatures, and it would also fire for sdk quickstart,
which has no stability flag at all. Not duplicated as an identical method on both prompt classes
either: two copies of a message required to stay identical will drift.

SDK customizations on v4 — targeted warning, plus a confirm in the wizard

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.

Today's warning is weak in three ways: it is unconditional (fires on every v4 run, including the
majority that have no customizations, which trains users to ignore it); its wording is a generic
capability note
rather than "your changes will be dropped"; and nothing blocks — it is a
log.warn followed by the generation spinner and, in publish, an irreversible registry push.

hasSdkSourceTree is already computed in the same method (generate.ts:97) and is in scope at line
110. Use it:

  • Both commands, both paths: keep the generic notice when there is nothing to lose; emit a
    specific message naming the language when the build does have a tracked source tree.
  • Interactive publish only: a blocking confirm when a tracked source tree exists and V4 was
    selected; declining returns ActionResult.cancelled().

The confirm is wizard-only because interactive publish ends in an unreversible push reached by
picking a menu item, while a CI user typed the flag deliberately and cannot answer a prompt.
Overloading --force (which means "overwrite the destination directory") to also mean "discard my
customizations" would be a bad conflation.

The targeted message lives in GenerateAction, so sdk generate benefits too.
sdk quickstart is unaffected in practice — actions/sdk/quickstart.ts:204 hardcodes
CodeGenerationVersion.V3 with no flag to change it, so its v4 branch is unreachable.

Interactive mode gets a codegen prompt (PR 2)

An earlier draft stated the wizard could not produce a v4 SDK, on the grounds that
interactive = this.argv.length === 0 (src/commands/sdk/publish.ts:86) means no flags were
passed. Reversed — the wizard gets its own prompt instead of inheriting a flag:

  • Choice labels are V3 and V4 (beta), so the beta nature of v4 is visible at the moment of
    choosing. Stability is never prompted: it does not apply to v3, and picking V4 (beta) is the
    consent to beta. A later CLI version can offer a stability choice once v4 has more than one tier.

  • Default/initial selection is V3, so an inattentive Enter never lands on beta.

  • Placement: after selectPublishingProfile (interactive.ts:65) and before selectLanguage
    (75)
    , because it filters the language list.

  • Choosing V4 narrows the language list to csharp, via a single named constant:

    // Interactive only. The V4 generator currently supports csharp alone; the wizard filters to keep
    // the choice honest. Non-interactive deliberately does NOT filter — the service is the authority
    // there. Update when V4 adds languages.
    export const V4_INTERACTIVE_LANGUAGES: readonly Language[] = [Language.CSHARP];

    Filtering here does not repeat the shipped-binary trap: a wizard choice cannot be committed to
    a pipeline, so a stale list costs discoverability (fixed by upgrading), not correctness. The
    non-interactive path stays unfiltered and server-authoritative. The comment above is load-bearing
    without it the next reader sees a language filter and "fixes" the non-interactive path to match.

  • Empty intersection fails. A profile with no csharp configuration plus a V4 choice offers
    nothing; exit ActionResult.failed() (1) with a message naming the reason and pointing at
    configuring a csharp package in the App. This matches how the wizard already treats unmet
    preconditions — no active publishing profiles fails outright at interactive.ts:59-62. Hiding the
    V4 (beta) option instead would make it indistinguishable from v4 not existing.

  • Stability is derived, not prompted: V4 → BETA, V3 → STABLE, supplied by the interactive
    action to SdkPublishAction.

Out of scope

  • A codegen prompt or --stability handling in sdk quickstart — v3-only, unreachable v4 branch.
  • Pulling sdk generate into the v4/stability coercion debategenerate keeps its existing
    --stability flag semantics; it only gains the shared inert-on-v3 warning and the targeted
    customizations message.
  • Anything touching --version / SemVersion — see the assumption above.
  • A params-object refactor of the long positional signatures. .ai/skills/action.md has no
    convention here, and a swap of codegenVersion/stability is caught by the type checker since
    they are distinct enums.

Delivery — two PRs

PR 1 — non-interactive. This is the piece #304 depends on.

  • --codegen-version and --stability flags on sdk publish
  • threading: commands/sdk/publish.tsSdkPublishNonInteractiveActionSdkPublishAction
    GenerateAction
  • hardcoded V3 / STABLE deleted from SdkPublishAction (publish.ts:58-59)
  • shared warnIfStabilityIgnored, wired into both sdk generate and sdk publish
  • targeted SDK-customizations message in GenerateAction
  • telemetry payload, example, tests

Seam: SdkPublishAction.execute is called by both action classes, so once the literals become
parameters the interactive caller must pass something. In PR 1 it passes
CodeGenerationVersion.V3, Stability.STABLE explicitly — behaviour identical to today, the literals
simply move up one layer. No dead code, no wizard behaviour change.

PR 2 — interactive.

  • selectCodegenVersion prompt (V3 / V4 (beta), initial V3) after selectPublishingProfile
  • csharp-only language filtering via V4_INTERACTIVE_LANGUAGES
  • hard fail on empty intersection
  • blocking confirm when the build has a tracked source tree and V4 was chosen
  • BETA derived from the V4 choice, replacing PR 1's explicit V3, STABLE arguments

Change detail — PR 1

src/commands/sdk/publish.ts

Import: import { CodeGenerationVersion, Language, Stability } from '../../types/sdk/generate.js';

Flags — added last, after 'dry-run'. Name, description, options and default copied
verbatim from src/commands/sdk/generate.ts:46-55 so the two commands stay consistent:

    'codegen-version': Flags.string({
      description: 'Version of the code generator to use',
      options: Object.values(CodeGenerationVersion).map((v) => v.valueOf()),
      default: CodeGenerationVersion.V3
    }),
    'stability': Flags.string({
      description: 'Stability level of the generated SDK',
      options: Object.values(Stability).map((s) => s.valueOf()),
      default: Stability.STABLE
    })

Example — one v4 example added to static examples, including both flags, since that is the
only combination that works:

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

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

Destructure — both flags plus metadata; call the shared warning before dispatching.

Telemetry — add both flags to SdkPublishValidationFailedEvent so a failed v4 publish is
distinguishable from a failed v3 one. Its constructor takes flags: Record<string, unknown>
(src/types/events/sdk-publish-validation-failed.ts:6), so no type change is needed.

Dispatch — non-interactive receives both parsed values; interactive receives neither and passes
V3 / STABLE internally in PR 1. The as CodeGenerationVersion / as Stability casts match how
language as Language is already handled here and how sdk generate does it.

src/actions/sdk/publish/non-interactive.ts

Import CodeGenerationVersion and Stability; add both parameters after dryRun, keeping required
parameters ahead of the callback and the trailing optionals; forward both to SdkPublishAction.
Everything above that call — directory checks, missing-flag collection, version and profile-id
validation, profile lookup, language and publish-type checks — is unchanged.

src/actions/sdk/publish.ts

Add codegenVersion: CodeGenerationVersion and stability: Stability to execute, after dryRun.
Replace the two hardcoded literals with them:

      const sdkGenerationResult = await sdkGenerateAction.execute(
        buildDirectory,
        outputDirectory,
        language,
        force,
        false,
        false,
        false,
        codegenVersion,
        stability,
        undefined,
        semVersion,
        packageSettingsDirectory
      );

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

src/actions/sdk/generate.ts

Targeted customizations message only (see Design decisions). No signature change — it already
takes codeGenVersion and stability.

src/commands/sdk/generate.ts

Take metadata off the parse result and call warnIfStabilityIgnored. No other change.


Prototype run

A scratch-branch run against api.apimatic.io exercised the threading end to end:

◇  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 shows the value threaded through all four layers and reached
POST /sdk/v2. Without beta stability the same run failed at generation with the error quoted
above — which is what established that stability must be settable from publish at all.

This is a prototype, not a merge gate. It reached Publishing initiated, which proves the zip
was accepted — not what landed on the registry. Per The package version must come from the
generator
, what lands there today would carry the wrong version and the wrong package identity.

Known limitations

1. Package identity is wrong on the v4 path. SdkPublishAction writes
package-settings/{language}.json into the build zip, and the CLI ships it on the v4 path too —
getBuildZipPath(tempDirectory, packageSettingsDirectory) is called at generate.ts:106, before
the v4/v3 branch. The v4 generator does not consume it. Per the section above, publishing does
not compensate either, so the generated .csproj, assembly name and README install snippet carry a
default identity rather than the configured packageId — and for --publish-type sourcecode, that
content is what gets pushed to the customer's repository. The file carries package identity
only, not the version (package-settings-context.ts:17-22), so this is a separate defect from the
version problem and needs its own fix on the generator side.

3. The C# publish worker's project glob may not match a v4 SDK.
dotnet pack /sdk/*.Standard/*.Standard.csproj (CSharpPublishParams.cs:34) hardcodes the v3
generator's project layout and .Standard naming. If the v4 generator emits a different directory
or project name, the glob matches nothing and the C# publish fails in the container — after the CLI
has already reported Publishing initiated and returned success. Worth confirming against a real v4
SDK's output before v4 publishing is offered to users.

2. v4 and tracked SDK customizations. Mitigated but not solved — see Design decisions. The
build still generates and publishes without the tracked changes; the change here is that the warning
becomes specific and the wizard blocks. If a hard guard is added later, 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