From 9ad3157d3907f038927ef6cb89768bf1cb94cee0 Mon Sep 17 00:00:00 2001 From: Muhammad Rafay Nadeem Date: Wed, 12 Aug 2026 11:31:46 +0500 Subject: [PATCH 01/19] add non-interactive v4 sdk publish --- src/actions/sdk/publish.ts | 6 ++-- src/actions/sdk/publish/interactive.ts | 3 ++ src/actions/sdk/publish/non-interactive.ts | 6 +++- src/commands/sdk/generate.ts | 5 ++- src/commands/sdk/publish.ts | 41 +++++++++++++++++++--- src/prompts/sdk/stability.ts | 19 ++++++++++ test/prompts/sdk/stability.test.ts | 38 ++++++++++++++++++++ 7 files changed, 109 insertions(+), 9 deletions(-) create mode 100644 src/prompts/sdk/stability.ts create mode 100644 test/prompts/sdk/stability.test.ts diff --git a/src/actions/sdk/publish.ts b/src/actions/sdk/publish.ts index 086301cf..41e21e3d 100644 --- a/src/actions/sdk/publish.ts +++ b/src/actions/sdk/publish.ts @@ -34,6 +34,8 @@ export class SdkPublishAction { semVersion: SemVersion, publishingProfile: PublishingProfile, dryRun: boolean, + codegenVersion: CodeGenerationVersion, + stability: Stability, onPublishSdkError: (errorMessage: string) => void ): Promise => { const publishResult = await withDirPath(async (tempDirectory): Promise> => { @@ -55,8 +57,8 @@ export class SdkPublishAction { false, false, false, - CodeGenerationVersion.V3, - Stability.STABLE, + codegenVersion, + stability, undefined, semVersion, packageSettingsDirectory diff --git a/src/actions/sdk/publish/interactive.ts b/src/actions/sdk/publish/interactive.ts index 1344b0d8..3d3ce4b7 100644 --- a/src/actions/sdk/publish/interactive.ts +++ b/src/actions/sdk/publish/interactive.ts @@ -5,6 +5,7 @@ import { DirectoryPath } from '../../../types/file/directoryPath.js'; import { PublishType } from '../../../types/publish-api/publishing-profile-item.js'; import { PublishingProfile } from '../../../types/publish/publishing-profile.js'; import { PublishingProfiles } from '../../../types/publish/publishing-profiles.js'; +import { CodeGenerationVersion, Stability } from '../../../types/sdk/generate.js'; import { ActionResult } from '../../action-result.js'; import { SdkPublishAction } from '../publish.js'; import { BuildContext } from '../../../types/build-context.js'; @@ -109,6 +110,8 @@ export class SdkPublishInteractiveAction { version, publishingProfile, false, + CodeGenerationVersion.V3, + Stability.STABLE, onPublishSdkError ); if (publishResult.isFailed()) { diff --git a/src/actions/sdk/publish/non-interactive.ts b/src/actions/sdk/publish/non-interactive.ts index fb6e5ca9..f995beb1 100644 --- a/src/actions/sdk/publish/non-interactive.ts +++ b/src/actions/sdk/publish/non-interactive.ts @@ -4,7 +4,7 @@ import { CommandMetadata } from '../../../types/common/command-metadata.js'; import { DirectoryPath } from '../../../types/file/directoryPath.js'; import { PublishingProfileItem, PublishType } from '../../../types/publish-api/publishing-profile-item.js'; import { PublishingProfile } from '../../../types/publish/publishing-profile.js'; -import { Language } from '../../../types/sdk/generate.js'; +import { CodeGenerationVersion, Language, Stability } from '../../../types/sdk/generate.js'; import { ActionResult } from '../../action-result.js'; import { getDownloadsDirectory } from '../../../infrastructure/os-extensions.js'; import { SemVersion } from '../../../types/publish/version.js'; @@ -27,6 +27,8 @@ export class SdkPublishNonInteractiveAction { publishTypes: PublishType[], force: boolean, dryRun: boolean, + codegenVersion: CodeGenerationVersion, + stability: Stability, onPublishSdkError: (errorMessage: string) => void, profileId?: string, version?: string @@ -113,6 +115,8 @@ export class SdkPublishNonInteractiveAction { semVersion, publishingProfile, dryRun, + codegenVersion, + stability, onPublishSdkError ); if (publishResult.isFailed()) { diff --git a/src/commands/sdk/generate.ts b/src/commands/sdk/generate.ts index f24fdcc8..fe2aae4e 100644 --- a/src/commands/sdk/generate.ts +++ b/src/commands/sdk/generate.ts @@ -8,6 +8,7 @@ import { format, intro, outro } from "../../prompts/format.js"; import { SdkChangesTrackedEvent } from "../../types/events/sdk-changes-tracked.js"; import { TelemetryService } from "../../infrastructure/services/telemetry-service.js"; import { SdkConflictsResolvedEvent } from "../../types/events/sdk-conflicts-resolved.js"; +import { warnIfStabilityIgnored } from "../../prompts/sdk/stability.js"; export default class SdkGenerate extends Command { static readonly summary = "Generate an SDK for your API"; @@ -79,7 +80,8 @@ Supports multiple programming languages including Java, C#, Python, JavaScript, "api-version": apiVersion, "codegen-version": codegenVersion, stability - } + }, + metadata } = await this.parse(SdkGenerate); const workingDirectory = DirectoryPath.createInput(input); @@ -93,6 +95,7 @@ Supports multiple programming languages including Java, C#, Python, JavaScript, const telemetryService = new TelemetryService(this.getConfigDir()); intro("Generate SDK"); + warnIfStabilityIgnored(codegenVersion as CodeGenerationVersion, metadata.flags.stability?.setFromDefault !== true); const action = new GenerateAction(this.getConfigDir(), commandMetadata, authKey); const result = await action.execute( buildDirectory, diff --git a/src/commands/sdk/publish.ts b/src/commands/sdk/publish.ts index b16385f4..f24e84e0 100644 --- a/src/commands/sdk/publish.ts +++ b/src/commands/sdk/publish.ts @@ -1,7 +1,8 @@ import { Command, Flags } from '@oclif/core'; import { DirectoryPath } from '../../types/file/directoryPath.js'; import { FlagsProvider } from '../../types/flags-provider.js'; -import { Language } from '../../types/sdk/generate.js'; +import { CodeGenerationVersion, Language, Stability } from '../../types/sdk/generate.js'; +import { warnIfStabilityIgnored } from '../../prompts/sdk/stability.js'; import { CommandMetadata } from '../../types/common/command-metadata.js'; import { format, intro, outro } from '../../prompts/format.js'; import { PublishType } from '../../types/publish-api/publishing-profile-item.js'; @@ -46,6 +47,16 @@ export default class SdkPublish extends Command { '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 + }), + 'stability': Flags.string({ + description: 'Stability level of the generated SDK', + options: Object.values(Stability).map((s) => s.valueOf()), + default: Stability.STABLE }) }; @@ -65,7 +76,14 @@ export default class SdkPublish extends Command { `${SdkPublish.cmdTxt} ${format.flag('profile-id', 'c3d4e5f6a1b2c3d4e5f6a1b2')} ${format.flag('language', 'python')} ${format.flag( 'version', '1.0.0' - )} ${format.flag('publish-type', PublishType.PackagePublishing)} ${format.flag('dry-run')}` + )} ${format.flag('publish-type', PublishType.PackagePublishing)} ${format.flag('dry-run')}`, + `${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')}` ]; async run() { @@ -78,8 +96,11 @@ export default class SdkPublish extends Command { force, input, 'publish-type': publishType, - 'dry-run': dryRun - } + 'dry-run': dryRun, + 'codegen-version': codegenVersion, + stability + }, + metadata } = await this.parse(SdkPublish); const publishTypes = [...new Set(publishType)] as PublishType[]; @@ -108,12 +129,20 @@ export default class SdkPublish extends Command { version, language, ...(force && { force }), - 'publish-type': publishTypes + 'publish-type': publishTypes, + 'codegen-version': codegenVersion, + stability }), commandMetadata.shell ); intro('Publish SDK'); + if (!interactive) { + warnIfStabilityIgnored( + codegenVersion as CodeGenerationVersion, + metadata.flags.stability?.setFromDefault !== true + ); + } const result = interactive ? await new SdkPublishInteractiveAction(configDir, commandMetadata).execute( workingDirectory, @@ -126,6 +155,8 @@ export default class SdkPublish extends Command { publishTypes, force, dryRun, + codegenVersion as CodeGenerationVersion, + stability as Stability, onPublishSdkError, profileId, version, diff --git a/src/prompts/sdk/stability.ts b/src/prompts/sdk/stability.ts new file mode 100644 index 00000000..4a8b97b3 --- /dev/null +++ b/src/prompts/sdk/stability.ts @@ -0,0 +1,19 @@ +import { log } from "@clack/prompts"; +import { format as f } from "../format.js"; +import { CodeGenerationVersion } from "../../types/sdk/generate.js"; + +/** + * Shared by `sdk generate` and `sdk publish`. Lives at the command layer because only the parse + * result knows whether the user typed `--stability` or inherited its default, and firing solely on + * a typed flag is what keeps this from becoming noise. + */ +export function warnIfStabilityIgnored(codegenVersion: CodeGenerationVersion, stabilityWasProvided: boolean) { + if (!stabilityWasProvided || codegenVersion !== CodeGenerationVersion.V3) { + return; + } + + log.warn( + `${f.flag("stability")} has no effect with ${f.flag("codegen-version", CodeGenerationVersion.V3)}. ` + + `The V3 code generator always produces a stable SDK.` + ); +} diff --git a/test/prompts/sdk/stability.test.ts b/test/prompts/sdk/stability.test.ts new file mode 100644 index 00000000..ece45252 --- /dev/null +++ b/test/prompts/sdk/stability.test.ts @@ -0,0 +1,38 @@ +import { expect } from "chai"; +import sinon from "sinon"; +import { log } from "@clack/prompts"; +import { warnIfStabilityIgnored } from "../../../src/prompts/sdk/stability.js"; +import { CodeGenerationVersion } from "../../../src/types/sdk/generate.js"; + +// The V3 generation path takes no stability parameter, so `--stability` on V3 is accepted, +// never sent, and returns a stable SDK while the user believes otherwise. The trigger is +// typed-and-V3 rather than value-is-beta: `--stability stable` is equally ignored. +describe("warnIfStabilityIgnored", () => { + let warn: sinon.SinonStub; + + beforeEach(() => { + warn = sinon.stub(log, "warn"); + }); + + afterEach(() => { + warn.restore(); + }); + + it("warns when the flag was typed alongside codegen version v3", () => { + warnIfStabilityIgnored(CodeGenerationVersion.V3, true); + + expect(warn.calledOnce).to.equal(true); + }); + + it("stays silent when the flag was only filled in from its default", () => { + warnIfStabilityIgnored(CodeGenerationVersion.V3, false); + + expect(warn.called).to.equal(false); + }); + + it("stays silent on v4, where stability reaches the generation service", () => { + warnIfStabilityIgnored(CodeGenerationVersion.V4, true); + + expect(warn.called).to.equal(false); + }); +}); From 141da0519494e37e1176cdc32e9ccf448f9f6dad Mon Sep 17 00:00:00 2001 From: Muhammad Rafay Nadeem Date: Thu, 13 Aug 2026 12:22:27 +0500 Subject: [PATCH 02/19] feat: interactive sdk v4 support extracted common helper for publishing summary removed publish type sorting in helper --- src/actions/sdk/publish.ts | 14 ++++- src/actions/sdk/publish/interactive.ts | 25 +++++++-- src/prompts/sdk/publish.ts | 56 +++++++++++++------ src/prompts/sdk/publish/interactive.ts | 35 +++++++----- src/types/sdk/generate.ts | 32 +++++++++++ src/types/utils.ts | 2 + test/commands/sdk/publish-flags.test.ts | 48 ++++++++++++++++ .../sdk/publish/publishing-details.test.ts | 41 ++++++++++++++ 8 files changed, 216 insertions(+), 37 deletions(-) create mode 100644 test/commands/sdk/publish-flags.test.ts create mode 100644 test/prompts/sdk/publish/publishing-details.test.ts diff --git a/src/actions/sdk/publish.ts b/src/actions/sdk/publish.ts index 41e21e3d..76b206ec 100644 --- a/src/actions/sdk/publish.ts +++ b/src/actions/sdk/publish.ts @@ -1,7 +1,7 @@ import { PublishingApiService } from '../../infrastructure/services/publishing-api-service.js'; import { withDirPath } from '../../infrastructure/tmp-extensions.js'; import { LauncherService } from '../../infrastructure/launcher-service.js'; -import { SdkPublishPrompts } from '../../prompts/sdk/publish.js'; +import { PublishingDetails, SdkPublishPrompts } from '../../prompts/sdk/publish.js'; import { CommandMetadata } from '../../types/common/command-metadata.js'; import { DirectoryPath } from '../../types/file/directoryPath.js'; import { PublishType } from '../../types/publish-api/publishing-profile-item.js'; @@ -38,6 +38,14 @@ export class SdkPublishAction { stability: Stability, onPublishSdkError: (errorMessage: string) => void ): Promise => { + const publishingDetails: PublishingDetails = { + profile: publishingProfile, + language, + version: semVersion, + publishType, + codegenOption: { version: codegenVersion, stability } + }; + const publishResult = await withDirPath(async (tempDirectory): Promise> => { const packageConfigurationData = publishingProfile.getPackageConfigurationDataForLanguage(language); let packageSettingsDirectory: DirectoryPath | undefined; @@ -73,7 +81,7 @@ export class SdkPublishAction { const sdkLanguageDirectory = outputDirectory.join(language); if (dryRun) { - this.prompts.dryRunNotice(publishingProfile, language, semVersion, publishType); + this.prompts.dryRunNotice(publishingDetails); const readmeFilePath = new FilePath(sdkLanguageDirectory, new FileName('README.md')); await this.launcherService.openDirectoryInEditorOrFileExplorer(sdkLanguageDirectory, readmeFilePath); return ActionResult.success(); @@ -116,7 +124,7 @@ export class SdkPublishAction { } const publishingInfo = publishResult.getValue(); - this.prompts.publishingRunningNotice(publishingProfile, language, semVersion, publishType); + this.prompts.publishingRunningNotice(publishingDetails); this.prompts.publishingLogsMessage(publishingInfo.publishingLogUrl); const publishingOutcome = await this.prompts.pollPublishingStatus(() => diff --git a/src/actions/sdk/publish/interactive.ts b/src/actions/sdk/publish/interactive.ts index 3d3ce4b7..307b3f8f 100644 --- a/src/actions/sdk/publish/interactive.ts +++ b/src/actions/sdk/publish/interactive.ts @@ -5,7 +5,7 @@ import { DirectoryPath } from '../../../types/file/directoryPath.js'; import { PublishType } from '../../../types/publish-api/publishing-profile-item.js'; import { PublishingProfile } from '../../../types/publish/publishing-profile.js'; import { PublishingProfiles } from '../../../types/publish/publishing-profiles.js'; -import { CodeGenerationVersion, Stability } from '../../../types/sdk/generate.js'; +import { getCodegenOptions } from '../../../types/sdk/generate.js'; import { ActionResult } from '../../action-result.js'; import { SdkPublishAction } from '../publish.js'; import { BuildContext } from '../../../types/build-context.js'; @@ -79,6 +79,17 @@ export class SdkPublishInteractiveAction { return ActionResult.cancelled(); } + const codegenOptions = getCodegenOptions(language); + let codegenOption = codegenOptions[0]; + if (codegenOptions.length > 1) { + const selectedCodegenOption = await this.prompts.selectCodegenVersion(codegenOptions); + if (!selectedCodegenOption) { + this.prompts.noCodegenVersionSelected(); + return ActionResult.cancelled(); + } + codegenOption = selectedCodegenOption; + } + const version = await this.prompts.inputVersion(); if (!version) { this.prompts.noVersionSpecified(); @@ -87,7 +98,13 @@ export class SdkPublishInteractiveAction { const publishTypes = publishingProfile.getPublishTypesForLanguage(language); - this.prompts.publishingSummary(publishingProfile, language, version, publishTypes); + this.prompts.publishingSummary({ + profile: publishingProfile, + language, + version, + publishType: publishTypes, + codegenOption + }); const confirmed = await this.prompts.confirmPublishing(); if (!confirmed) { @@ -110,8 +127,8 @@ export class SdkPublishInteractiveAction { version, publishingProfile, false, - CodeGenerationVersion.V3, - Stability.STABLE, + codegenOption.version, + codegenOption.stability, onPublishSdkError ); if (publishResult.isFailed()) { diff --git a/src/prompts/sdk/publish.ts b/src/prompts/sdk/publish.ts index 99d0bd4f..20403732 100644 --- a/src/prompts/sdk/publish.ts +++ b/src/prompts/sdk/publish.ts @@ -5,7 +5,7 @@ import { PublishLogItem } from '../../types/publish-api/publish-log.js'; import { PublishingInfo } from '../../types/publish-api/publishing-info.js'; import { PublishType } from '../../types/publish-api/publishing-profile-item.js'; import { SemVersion } from '../../types/publish/version.js'; -import { Language } from '../../types/sdk/generate.js'; +import { CodeGenerationVersion, CodegenOption, formatCodegenOption, Language } from '../../types/sdk/generate.js'; import { noteWrapped, withSpinner } from '../prompt.js'; import { format as f } from '../format.js'; import { PublishingProfile } from '../../types/publish/publishing-profile.js'; @@ -21,26 +21,15 @@ export class SdkPublishPrompts { log.error(serviceError.errorMessage); } - public dryRunNotice(publishingProfile: PublishingProfile, language: Language, version: SemVersion, publishType: PublishType[]): void { - const targets = publishType.map((t) => (t === PublishType.PackagePublishing ? 'Package' : 'Source Code')).join(' + '); + public dryRunNotice(details: PublishingDetails): void { log.info( - `You can publish this SDK by removing the --dry-run flag. It will be published for the following:\n\n Profile: ${publishingProfile}\n Language: ${language}\n Version: ${version}\n Targets: ${targets}` + `You can publish this SDK by removing the --dry-run flag. It will be published for the following:` + + formatPublishingDetails(details) ); } - public publishingRunningNotice( - profile: PublishingProfile, - language: Language, - version: SemVersion, - publishType: PublishType[] - ): void { - const targets = [...publishType] - .sort((a, b) => (a === PublishType.SourceCodePublishing ? -1 : b === PublishType.SourceCodePublishing ? 1 : 0)) - .map((t) => (t === PublishType.PackagePublishing ? 'Package' : 'Source Code')) - .join(' + '); - log.info( - `Publishing is running for the following:\n\n Profile: ${profile}\n Language: ${language}\n Version: ${version}\n Targets: ${targets}` - ); + public publishingRunningNotice(details: PublishingDetails): void { + log.info(`Publishing is running for the following:` + formatPublishingDetails(details)); } public publishingLogsMessage(publishingLogUrl: string) { @@ -109,3 +98,36 @@ ${f.link(publishingLogUrl)}`; return 'cancelled'; } } + +export interface PublishingDetails { + profile: PublishingProfile; + language: Language; + version: SemVersion; + publishType: PublishType[]; + codegenOption: CodegenOption; +} + +/** + * The detail block shared by the pre-publish summary, the dry-run notice and the running notice. + * Kept in one place so the three cannot drift apart again — they previously disagreed on whether + * to sort the targets. + */ +export function formatPublishingDetails({ + profile, + language, + version, + publishType, + codegenOption +}: PublishingDetails): string { + const targets = [...publishType] + .sort((a, b) => (a === PublishType.SourceCodePublishing ? -1 : b === PublishType.SourceCodePublishing ? 1 : 0)) + .map((t) => (t === PublishType.PackagePublishing ? "Package" : "Source Code")) + .join(" + "); + + // V3 is the default generator, so naming it would add a line to every pre-existing flow. The row + // earns its place only when something other than the default produced the SDK. + const generator = + codegenOption.version === CodeGenerationVersion.V3 ? "" : `\n Generator: ${formatCodegenOption(codegenOption)}`; + + return `\n\n Profile: ${profile}\n Language: ${language}\n Version: ${version}\n Targets: ${targets}${generator}`; +} diff --git a/src/prompts/sdk/publish/interactive.ts b/src/prompts/sdk/publish/interactive.ts index 601c4479..2f4af1c9 100644 --- a/src/prompts/sdk/publish/interactive.ts +++ b/src/prompts/sdk/publish/interactive.ts @@ -10,9 +10,10 @@ import { PublishType } from '../../../types/publish-api/publishing-profile-item.js'; import { PublishingProfile } from '../../../types/publish/publishing-profile.js'; -import { Language } from '../../../types/sdk/generate.js'; +import { CodegenOption, formatCodegenOption, Language } from '../../../types/sdk/generate.js'; import { SemVersion } from '../../../types/publish/version.js'; import { removeQuotes } from '../../../utils/string-utils.js'; +import { formatPublishingDetails, PublishingDetails } from '../publish.js'; export class SdkPublishInteractivePrompts { public async inputWorkingDirectory( @@ -142,6 +143,24 @@ export class SdkPublishInteractivePrompts { log.error('No language was selected for publishing.'); } + public async selectCodegenVersion(options: readonly CodegenOption[]): Promise { + const codegenOption = await select({ + message: 'Select the Code Generator version:', + initialValue: options[0], + options: options.map((option) => ({ value: option, label: formatCodegenOption(option) })) + }); + + if (isCancel(codegenOption)) { + return undefined; + } + + return codegenOption; + } + + public noCodegenVersionSelected() { + log.error('No Code Generator version was selected.'); + } + public async inputVersion(): Promise { const version = await text({ message: 'Enter version to publish (e.g. 1.0.0):', @@ -165,18 +184,8 @@ export class SdkPublishInteractivePrompts { log.error('No version was specified for publishing the SDK.'); } - public publishingSummary( - profile: PublishingProfile, - language: Language, - version: SemVersion, - publishType: PublishType[] - ) { - const targets = publishType - .map((t) => (t === PublishType.PackagePublishing ? 'Package' : 'Source Code')) - .join(' + '); - log.info( - `Ready to publish:\n\n Profile: ${profile}\n Language: ${language}\n Version: ${version}\n Targets: ${targets}` - ); + public publishingSummary(details: PublishingDetails) { + log.info(`Ready to publish:` + formatPublishingDetails(details)); } public async confirmPublishing(): Promise { diff --git a/src/types/sdk/generate.ts b/src/types/sdk/generate.ts index 106932c2..ae06490d 100644 --- a/src/types/sdk/generate.ts +++ b/src/types/sdk/generate.ts @@ -1,3 +1,5 @@ +import { NonEmptyArray } from "../utils.js"; + export enum Language { CSHARP = "csharp", JAVA = "java", @@ -49,3 +51,33 @@ export const LANGUAGE_CHOICES: ReadonlyArray<{ label: string; value: Language }> { label: "PHP", value: Language.PHP }, { label: "Go", value: Language.GO } ]; + +export interface CodegenOption { + version: CodeGenerationVersion; + stability: Stability; +} + +/** + * For validating only interactive `sdk publish/generate` commands. + * Non-interactive validation is handled server-side by codegen API. + */ +export const CODEGEN_OPTIONS: Readonly>>> = { + [Language.CSHARP]: [ + { version: CodeGenerationVersion.V3, stability: Stability.STABLE }, + { version: CodeGenerationVersion.V4, stability: Stability.BETA } + ], + [Language.GO]: [{ version: CodeGenerationVersion.V3, stability: Stability.STABLE }], + [Language.JAVA]: [{ version: CodeGenerationVersion.V3, stability: Stability.STABLE }], + [Language.PHP]: [{ version: CodeGenerationVersion.V3, stability: Stability.STABLE }], + [Language.PYTHON]: [{ version: CodeGenerationVersion.V3, stability: Stability.STABLE }], + [Language.RUBY]: [{ version: CodeGenerationVersion.V3, stability: Stability.STABLE }], + [Language.TYPESCRIPT]: [{ version: CodeGenerationVersion.V3, stability: Stability.STABLE }] +}; + +export function getCodegenOptions(language: Language): Readonly> { + return CODEGEN_OPTIONS[language]; +} + +export function formatCodegenOption({ version, stability }: CodegenOption): string { + return `${version.toUpperCase()} (${stability})`; +} diff --git a/src/types/utils.ts b/src/types/utils.ts index 9cef57d5..faf5a0dd 100644 --- a/src/types/utils.ts +++ b/src/types/utils.ts @@ -1,3 +1,5 @@ +export type NonEmptyArray = [T, ...T[]]; + export type ValidationMessages = { messages: string[]; warnings: string[]; diff --git a/test/commands/sdk/publish-flags.test.ts b/test/commands/sdk/publish-flags.test.ts new file mode 100644 index 00000000..1ac84299 --- /dev/null +++ b/test/commands/sdk/publish-flags.test.ts @@ -0,0 +1,48 @@ +import { expect } from "chai"; +import { Parser } from "@oclif/core"; +import SdkPublish from "../../../src/commands/sdk/publish.js"; +import { CodeGenerationVersion, Stability } from "../../../src/types/sdk/generate.js"; + +const parse = (argv: string[]) => Parser.parse(argv, { flags: SdkPublish.flags as never, strict: true } as never); + +// `warnIfStabilityIgnored` decides whether to fire from oclif's `setFromDefault` metadata rather +// than from the flag's value, because `--stability stable` on V3 is ignored just as silently as +// `--stability beta`. This is the codebase's first use of parse metadata, so the mechanism itself +// is worth pinning down: if oclif ever stops reporting it, the warning fails open and silently. +describe("sdk publish codegen flags", () => { + it("defaults to v3 and stable when neither flag is passed", async () => { + const { flags } = (await parse([])) as never as { flags: Record }; + + expect(flags["codegen-version"]).to.equal(CodeGenerationVersion.V3); + expect(flags.stability).to.equal(Stability.STABLE); + }); + + it("accepts the v4 combination", async () => { + const { flags } = (await parse(["--codegen-version", "v4", "--stability", "beta"])) as never as { + flags: Record; + }; + + expect(flags["codegen-version"]).to.equal(CodeGenerationVersion.V4); + expect(flags.stability).to.equal(Stability.BETA); + }); + + it("rejects a codegen version the CLI does not know", async () => { + try { + await parse(["--codegen-version", "v5"]); + expect.fail("expected --codegen-version v5 to be rejected"); + } catch (error) { + expect((error as Error).message).to.contain("v5"); + } + }); + + it("does not filter languages by codegen version, leaving that to the service", async () => { + const { flags } = (await parse([ + "--codegen-version", + "v4", + "--language", + "typescript" + ])) as never as { flags: Record }; + + expect(flags.language).to.equal("typescript"); + }); +}); diff --git a/test/prompts/sdk/publish/publishing-details.test.ts b/test/prompts/sdk/publish/publishing-details.test.ts new file mode 100644 index 00000000..59cf5712 --- /dev/null +++ b/test/prompts/sdk/publish/publishing-details.test.ts @@ -0,0 +1,41 @@ +import { expect } from "chai"; +import { formatPublishingDetails } from "../../../../src/prompts/sdk/publish.js"; +import { PublishType } from "../../../../src/types/publish-api/publishing-profile-item.js"; +import { PublishingProfile } from "../../../../src/types/publish/publishing-profile.js"; +import { SemVersion } from "../../../../src/types/publish/version.js"; +import { CodeGenerationVersion, Language, Stability } from "../../../../src/types/sdk/generate.js"; + +const profile = { toString: () => "My Profile" } as unknown as PublishingProfile; +const version = SemVersion.tryCreate("1.2.3")._unsafeUnwrap(); + +const details = (publishType: PublishType[], codegenVersion: CodeGenerationVersion, stability: Stability) => + formatPublishingDetails({ + profile, + language: Language.CSHARP, + version, + publishType, + codegenOption: { version: codegenVersion, stability } + }); + +describe("formatPublishingDetails", () => { + it("renders the profile, language, version and targets", () => { + const output = details([PublishType.PackagePublishing], CodeGenerationVersion.V3, Stability.STABLE); + + expect(output).to.contain("Profile: My Profile"); + expect(output).to.contain("Language: csharp"); + expect(output).to.contain("Version: 1.2.3"); + expect(output).to.contain("Targets: Package"); + }); + + it("omits the generator row for v3, leaving pre-existing output unchanged", () => { + const output = details([PublishType.PackagePublishing], CodeGenerationVersion.V3, Stability.STABLE); + + expect(output).to.not.contain("Generator:"); + }); + + it("names the generator once it is no longer the default", () => { + const output = details([PublishType.PackagePublishing], CodeGenerationVersion.V4, Stability.BETA); + + expect(output).to.contain("Generator: V4 (beta)"); + }); +}); From 2e5c5db00db6970cb5950c486fc8b5e22bd47764 Mon Sep 17 00:00:00 2001 From: Muhammad Rafay Nadeem Date: Thu, 13 Aug 2026 12:24:37 +0500 Subject: [PATCH 03/19] fix: remove sorting of publish types --- src/prompts/sdk/publish.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/prompts/sdk/publish.ts b/src/prompts/sdk/publish.ts index 20403732..af1fad6e 100644 --- a/src/prompts/sdk/publish.ts +++ b/src/prompts/sdk/publish.ts @@ -120,7 +120,6 @@ export function formatPublishingDetails({ codegenOption }: PublishingDetails): string { const targets = [...publishType] - .sort((a, b) => (a === PublishType.SourceCodePublishing ? -1 : b === PublishType.SourceCodePublishing ? 1 : 0)) .map((t) => (t === PublishType.PackagePublishing ? "Package" : "Source Code")) .join(" + "); From 2cc7900e3e253230a9369076290593a46bfbe7ea Mon Sep 17 00:00:00 2001 From: Muhammad Rafay Nadeem Date: Thu, 13 Aug 2026 12:24:56 +0500 Subject: [PATCH 04/19] doc: remove comment --- src/prompts/sdk/publish.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/prompts/sdk/publish.ts b/src/prompts/sdk/publish.ts index af1fad6e..56cb7ae3 100644 --- a/src/prompts/sdk/publish.ts +++ b/src/prompts/sdk/publish.ts @@ -107,11 +107,6 @@ export interface PublishingDetails { codegenOption: CodegenOption; } -/** - * The detail block shared by the pre-publish summary, the dry-run notice and the running notice. - * Kept in one place so the three cannot drift apart again — they previously disagreed on whether - * to sort the targets. - */ export function formatPublishingDetails({ profile, language, From 1b9cb4c4f31cff66c66ca1536f7ddd5c2a9174c7 Mon Sep 17 00:00:00 2001 From: Muhammad Rafay Nadeem Date: Thu, 13 Aug 2026 12:40:03 +0500 Subject: [PATCH 05/19] fix: show generator only when user chooses --- src/actions/sdk/publish/interactive.ts | 5 +++-- src/prompts/sdk/publish.ts | 8 ++------ 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/actions/sdk/publish/interactive.ts b/src/actions/sdk/publish/interactive.ts index 307b3f8f..7e70f2dd 100644 --- a/src/actions/sdk/publish/interactive.ts +++ b/src/actions/sdk/publish/interactive.ts @@ -81,7 +81,8 @@ export class SdkPublishInteractiveAction { const codegenOptions = getCodegenOptions(language); let codegenOption = codegenOptions[0]; - if (codegenOptions.length > 1) { + const userChoosesCodeGenVersion = codegenOptions.length > 1; + if (userChoosesCodeGenVersion) { const selectedCodegenOption = await this.prompts.selectCodegenVersion(codegenOptions); if (!selectedCodegenOption) { this.prompts.noCodegenVersionSelected(); @@ -103,7 +104,7 @@ export class SdkPublishInteractiveAction { language, version, publishType: publishTypes, - codegenOption + codegenOption: userChoosesCodeGenVersion ? codegenOption : undefined }); const confirmed = await this.prompts.confirmPublishing(); diff --git a/src/prompts/sdk/publish.ts b/src/prompts/sdk/publish.ts index 56cb7ae3..6e1e5d10 100644 --- a/src/prompts/sdk/publish.ts +++ b/src/prompts/sdk/publish.ts @@ -104,7 +104,7 @@ export interface PublishingDetails { language: Language; version: SemVersion; publishType: PublishType[]; - codegenOption: CodegenOption; + codegenOption?: CodegenOption; } export function formatPublishingDetails({ @@ -118,10 +118,6 @@ export function formatPublishingDetails({ .map((t) => (t === PublishType.PackagePublishing ? "Package" : "Source Code")) .join(" + "); - // V3 is the default generator, so naming it would add a line to every pre-existing flow. The row - // earns its place only when something other than the default produced the SDK. - const generator = - codegenOption.version === CodeGenerationVersion.V3 ? "" : `\n Generator: ${formatCodegenOption(codegenOption)}`; - + const generator = codegenOption ? `\n Generator: ${formatCodegenOption(codegenOption)}` : ""; return `\n\n Profile: ${profile}\n Language: ${language}\n Version: ${version}\n Targets: ${targets}${generator}`; } From 61fd7e29b5b123051232b42378c98262129c2748 Mon Sep 17 00:00:00 2001 From: Muhammad Rafay Nadeem Date: Thu, 13 Aug 2026 12:48:07 +0500 Subject: [PATCH 06/19] refactor: remove unused import --- src/prompts/sdk/publish.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/prompts/sdk/publish.ts b/src/prompts/sdk/publish.ts index 6e1e5d10..ee0eca27 100644 --- a/src/prompts/sdk/publish.ts +++ b/src/prompts/sdk/publish.ts @@ -5,7 +5,7 @@ import { PublishLogItem } from '../../types/publish-api/publish-log.js'; import { PublishingInfo } from '../../types/publish-api/publishing-info.js'; import { PublishType } from '../../types/publish-api/publishing-profile-item.js'; import { SemVersion } from '../../types/publish/version.js'; -import { CodeGenerationVersion, CodegenOption, formatCodegenOption, Language } from '../../types/sdk/generate.js'; +import { CodegenOption, formatCodegenOption, Language } from '../../types/sdk/generate.js'; import { noteWrapped, withSpinner } from '../prompt.js'; import { format as f } from '../format.js'; import { PublishingProfile } from '../../types/publish/publishing-profile.js'; From 7654efb71ff76d59228e138b443ae0e5cea25c73 Mon Sep 17 00:00:00 2001 From: Muhammad Rafay Nadeem Date: Thu, 13 Aug 2026 12:53:02 +0500 Subject: [PATCH 07/19] refactor: simplify codegen option selection --- src/actions/sdk/publish/interactive.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/actions/sdk/publish/interactive.ts b/src/actions/sdk/publish/interactive.ts index 7e70f2dd..c6c5dc6d 100644 --- a/src/actions/sdk/publish/interactive.ts +++ b/src/actions/sdk/publish/interactive.ts @@ -80,15 +80,11 @@ export class SdkPublishInteractiveAction { } const codegenOptions = getCodegenOptions(language); - let codegenOption = codegenOptions[0]; - const userChoosesCodeGenVersion = codegenOptions.length > 1; - if (userChoosesCodeGenVersion) { - const selectedCodegenOption = await this.prompts.selectCodegenVersion(codegenOptions); - if (!selectedCodegenOption) { + let codegenOption = codegenOptions.length === 1 ? codegenOptions[0] + : await this.prompts.selectCodegenVersion(codegenOptions); + if (!codegenOption) { this.prompts.noCodegenVersionSelected(); return ActionResult.cancelled(); - } - codegenOption = selectedCodegenOption; } const version = await this.prompts.inputVersion(); @@ -104,7 +100,7 @@ export class SdkPublishInteractiveAction { language, version, publishType: publishTypes, - codegenOption: userChoosesCodeGenVersion ? codegenOption : undefined + codegenOption: codegenOptions.length === 1 ? codegenOption : undefined }); const confirmed = await this.prompts.confirmPublishing(); From 8f0575c5776984f1bd68408608cf337bf378cd9c Mon Sep 17 00:00:00 2001 From: Muhammad Rafay Nadeem Date: Thu, 13 Aug 2026 12:54:11 +0500 Subject: [PATCH 08/19] fix: wrong logic --- src/actions/sdk/publish/interactive.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/actions/sdk/publish/interactive.ts b/src/actions/sdk/publish/interactive.ts index c6c5dc6d..869330f3 100644 --- a/src/actions/sdk/publish/interactive.ts +++ b/src/actions/sdk/publish/interactive.ts @@ -100,7 +100,7 @@ export class SdkPublishInteractiveAction { language, version, publishType: publishTypes, - codegenOption: codegenOptions.length === 1 ? codegenOption : undefined + codegenOption: codegenOptions.length === 1 ? undefined : codegenOption }); const confirmed = await this.prompts.confirmPublishing(); From a6b8d1bdc0f955d45de0c8d2f378fe569d50504b Mon Sep 17 00:00:00 2001 From: Muhammad Rafay Nadeem Date: Thu, 13 Aug 2026 13:55:25 +0500 Subject: [PATCH 09/19] test: assert generator row omitted when no codegen option given The formatter suppresses the row on a missing codegen option, not on v3; the interactive action decides when the user never chose a generator. Co-Authored-By: Claude Opus 5 (1M context) --- test/prompts/sdk/publish/publishing-details.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/prompts/sdk/publish/publishing-details.test.ts b/test/prompts/sdk/publish/publishing-details.test.ts index 59cf5712..428bd44c 100644 --- a/test/prompts/sdk/publish/publishing-details.test.ts +++ b/test/prompts/sdk/publish/publishing-details.test.ts @@ -8,13 +8,13 @@ import { CodeGenerationVersion, Language, Stability } from "../../../../src/type const profile = { toString: () => "My Profile" } as unknown as PublishingProfile; const version = SemVersion.tryCreate("1.2.3")._unsafeUnwrap(); -const details = (publishType: PublishType[], codegenVersion: CodeGenerationVersion, stability: Stability) => +const details = (publishType: PublishType[], codegenVersion?: CodeGenerationVersion, stability?: Stability) => formatPublishingDetails({ profile, language: Language.CSHARP, version, publishType, - codegenOption: { version: codegenVersion, stability } + codegenOption: codegenVersion && stability ? { version: codegenVersion, stability } : undefined }); describe("formatPublishingDetails", () => { @@ -27,8 +27,8 @@ describe("formatPublishingDetails", () => { expect(output).to.contain("Targets: Package"); }); - it("omits the generator row for v3, leaving pre-existing output unchanged", () => { - const output = details([PublishType.PackagePublishing], CodeGenerationVersion.V3, Stability.STABLE); + it("omits the generator row when no codegen option is given, leaving pre-existing output unchanged", () => { + const output = details([PublishType.PackagePublishing]); expect(output).to.not.contain("Generator:"); }); From ee47e8bfb70935105b3fc595af68a79cb4950d96 Mon Sep 17 00:00:00 2001 From: Muhammad Rafay Nadeem Date: Thu, 13 Aug 2026 13:55:46 +0500 Subject: [PATCH 10/19] fix: print publishing details once The interactive summary already lists the details before confirmation, so the running notice repeated them, and disagreed on the generator row. Co-Authored-By: Claude Opus 5 (1M context) --- src/actions/sdk/publish.ts | 16 +++++++--------- src/prompts/sdk/publish.ts | 4 ---- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/src/actions/sdk/publish.ts b/src/actions/sdk/publish.ts index 76b206ec..55aeef61 100644 --- a/src/actions/sdk/publish.ts +++ b/src/actions/sdk/publish.ts @@ -38,14 +38,6 @@ export class SdkPublishAction { stability: Stability, onPublishSdkError: (errorMessage: string) => void ): Promise => { - const publishingDetails: PublishingDetails = { - profile: publishingProfile, - language, - version: semVersion, - publishType, - codegenOption: { version: codegenVersion, stability } - }; - const publishResult = await withDirPath(async (tempDirectory): Promise> => { const packageConfigurationData = publishingProfile.getPackageConfigurationDataForLanguage(language); let packageSettingsDirectory: DirectoryPath | undefined; @@ -81,6 +73,13 @@ export class SdkPublishAction { const sdkLanguageDirectory = outputDirectory.join(language); if (dryRun) { + const publishingDetails: PublishingDetails = { + profile: publishingProfile, + language, + version: semVersion, + publishType, + codegenOption: { version: codegenVersion, stability } + }; this.prompts.dryRunNotice(publishingDetails); const readmeFilePath = new FilePath(sdkLanguageDirectory, new FileName('README.md')); await this.launcherService.openDirectoryInEditorOrFileExplorer(sdkLanguageDirectory, readmeFilePath); @@ -124,7 +123,6 @@ export class SdkPublishAction { } const publishingInfo = publishResult.getValue(); - this.prompts.publishingRunningNotice(publishingDetails); this.prompts.publishingLogsMessage(publishingInfo.publishingLogUrl); const publishingOutcome = await this.prompts.pollPublishingStatus(() => diff --git a/src/prompts/sdk/publish.ts b/src/prompts/sdk/publish.ts index ee0eca27..064cdb8e 100644 --- a/src/prompts/sdk/publish.ts +++ b/src/prompts/sdk/publish.ts @@ -28,10 +28,6 @@ export class SdkPublishPrompts { ); } - public publishingRunningNotice(details: PublishingDetails): void { - log.info(`Publishing is running for the following:` + formatPublishingDetails(details)); - } - public publishingLogsMessage(publishingLogUrl: string) { const message = `To track progress and view publishing logs, please visit: ${f.link(publishingLogUrl)}`; From cb06bfd03817359696750828bafb5fe2fb019ab8 Mon Sep 17 00:00:00 2001 From: Muhammad Rafay Nadeem Date: Thu, 13 Aug 2026 13:55:54 +0500 Subject: [PATCH 11/19] doc: add codegen version and stability flags to sdk publish Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 08fef0d8..ee145136 100644 --- a/README.md +++ b/README.md @@ -477,7 +477,7 @@ Generate and publish an SDK to a package registry and/or source repository ``` USAGE $ apimatic sdk publish [-p ] [-v ] [-d ] [-l csharp|java|php|python|ruby|typescript] [-f] - [-i ] [--publish-type package|sourcecode...] [--dry-run] + [-i ] [--publish-type package|sourcecode...] [--dry-run] [--codegen-version v3|v4] [--stability stable|beta] FLAGS -d, --destination= [default: /sdk] path where the sdk will be generated. @@ -488,10 +488,14 @@ FLAGS -p, --profile-id= Id of the publishing profile to use. -v, --version= Semantic version of the SDK to publish (e.g. 1.0.0). + --codegen-version=