diff --git a/src/actions/sdk/publish.ts b/src/actions/sdk/publish.ts index c736fe1c..086301cf 100644 --- a/src/actions/sdk/publish.ts +++ b/src/actions/sdk/publish.ts @@ -35,8 +35,8 @@ export class SdkPublishAction { publishingProfile: PublishingProfile, dryRun: boolean, onPublishSdkError: (errorMessage: string) => void - ): Promise> => { - return await withDirPath(async (tempDirectory) => { + ): Promise => { + const publishResult = await withDirPath(async (tempDirectory): Promise> => { const packageConfigurationData = publishingProfile.getPackageConfigurationDataForLanguage(language); let packageSettingsDirectory: DirectoryPath | undefined; @@ -100,5 +100,38 @@ export class SdkPublishAction { return ActionResult.success(publishSdkResponse.value); }); + + if (publishResult.isFailed()) { + return ActionResult.failed(); + } + if (publishResult.isCancelled()) { + return ActionResult.cancelled(); + } + + // Since publishing was not initiated, skip the next steps for polling. + if (dryRun) { + return ActionResult.success(); + } + + const publishingInfo = publishResult.getValue(); + this.prompts.publishingRunningNotice(publishingProfile, language, semVersion, publishType); + this.prompts.publishingLogsMessage(publishingInfo.publishingLogUrl); + + const publishingOutcome = await this.prompts.pollPublishingStatus(() => + this.publishingApiService.getSdkPublishingLog( + publishingInfo.publishLogId, + this.configDir, + this.commandMetadata.shell + ) + ); + + switch (publishingOutcome) { + case 'succeeded': + return ActionResult.success(); + case 'cancelled': + return ActionResult.cancelled(); + default: + return ActionResult.failed(); + } }; } diff --git a/src/actions/sdk/publish/interactive.ts b/src/actions/sdk/publish/interactive.ts index d170df7f..1344b0d8 100644 --- a/src/actions/sdk/publish/interactive.ts +++ b/src/actions/sdk/publish/interactive.ts @@ -118,7 +118,6 @@ export class SdkPublishInteractiveAction { return ActionResult.cancelled(); } - this.prompts.sdkPublishingInProgress(publishResult.getValue().publishingLogUrl); return ActionResult.success(); }; diff --git a/src/actions/sdk/publish/non-interactive.ts b/src/actions/sdk/publish/non-interactive.ts index e2b030ad..fb6e5ca9 100644 --- a/src/actions/sdk/publish/non-interactive.ts +++ b/src/actions/sdk/publish/non-interactive.ts @@ -122,28 +122,6 @@ export class SdkPublishNonInteractiveAction { return ActionResult.cancelled(); } - // Since publishing was not initiated, skip the next steps for polling. - if (dryRun) { - return ActionResult.success(); - } - - const publishingInfo = publishResult.getValue(); - this.prompts.publishingRunningNotice(publishingProfile, language, semVersion, publishTypes); - - const publishingSucceeded = await this.prompts.pollPublishingStatus(() => - this.publishingApiService.getSdkPublishingLog( - publishingInfo.publishLogId, - this.configDir, - this.commandMetadata.shell - ) - ); - - this.prompts.postPublishingMessage(publishingInfo.publishingLogUrl); - - if (!publishingSucceeded) { - return ActionResult.failed(); - } - return ActionResult.success(); }; } diff --git a/src/prompts/sdk/publish.ts b/src/prompts/sdk/publish.ts index ac785e46..99d0bd4f 100644 --- a/src/prompts/sdk/publish.ts +++ b/src/prompts/sdk/publish.ts @@ -1,26 +1,111 @@ -import { log } from '@clack/prompts'; -import { Result } from 'neverthrow'; -import { ServiceError } from '../../infrastructure/service-error.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 { withSpinner } from '../prompt.js'; -import { PublishingProfile } from '../../types/publish/publishing-profile.js'; - -export class SdkPublishPrompts { - public publishSdk(fn: Promise>) { - return withSpinner('Publishing SDK', 'Publishing initiated.', 'SDK Publishing failed.', fn); - } - - public sdkPublishingServiceError(serviceError: ServiceError) { - 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(' + '); - 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}` - ); - } -} +import { log, spinner } from '@clack/prompts'; +import { Result } from 'neverthrow'; +import { ServiceError } from '../../infrastructure/service-error.js'; +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 { noteWrapped, withSpinner } from '../prompt.js'; +import { format as f } from '../format.js'; +import { PublishingProfile } from '../../types/publish/publishing-profile.js'; + +export type PublishingOutcome = 'succeeded' | 'failed' | 'cancelled'; + +export class SdkPublishPrompts { + public publishSdk(fn: Promise>) { + return withSpinner('Publishing SDK', 'Publishing initiated.', 'SDK Publishing failed.', fn); + } + + public sdkPublishingServiceError(serviceError: ServiceError) { + 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(' + '); + 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}` + ); + } + + 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 publishingLogsMessage(publishingLogUrl: string) { + const message = `To track progress and view publishing logs, please visit: +${f.link(publishingLogUrl)}`; + noteWrapped(message, 'Publishing Logs'); + } + + public async pollPublishingStatus( + getSdkPublishingLogFn: () => Promise> + ): Promise { + const TERMINAL_STATES = new Set(['Succeeded', 'Failed', 'Exception', 'InternalError']); + const POLL_INTERVAL_MS = 10000; // poll after every 10 seconds. + + let abortWait: (() => void) | undefined; + const spin = spinner({ + onCancel: () => { + abortWait?.(); + }, + cancelMessage: 'Publishing is still running on APIMatic and will continue without the CLI.' + }); + + spin.start('Waiting for publishing status...'); + + while (!spin.isCancelled) { + const publishingLogResult = await getSdkPublishingLogFn(); + + if (publishingLogResult.isErr()) { + spin.stop('Failed to fetch publishing status.', 1); + return 'failed'; + } + + const { events } = publishingLogResult.value; + const executionCompleted = events.every((event) => TERMINAL_STATES.has(event.eventType)); + const statusMessage = [...events] + .sort((a, b) => (a.publishType === 'SourceCode' ? -1 : b.publishType === 'SourceCode' ? 1 : 0)) + .map((event) => { + const target = event.publishType === 'SourceCode' ? 'Source Code' : 'Package'; + const eventLabels: Record = { + Queued: 'Queued', + InProgress: 'In Progress', + Succeeded: 'Published' + }; + const label = eventLabels[event.eventType] ?? 'Failed'; + return `${target}: [${label}]`; + }) + .join(' | '); + + if (executionCompleted) { + const isExecutionSuccessful = events.every((event) => event.eventType === 'Succeeded'); + spin.stop(statusMessage, isExecutionSuccessful ? 0 : 1); + return isExecutionSuccessful ? 'succeeded' : 'failed'; + } + + spin.message(statusMessage); + await new Promise((resolve) => { + const timer = setTimeout(resolve, POLL_INTERVAL_MS); + abortWait = () => { + clearTimeout(timer); + resolve(); + }; + }); + abortWait = undefined; + } + + return 'cancelled'; + } +} diff --git a/src/prompts/sdk/publish/interactive.ts b/src/prompts/sdk/publish/interactive.ts index a946ac7d..601c4479 100644 --- a/src/prompts/sdk/publish/interactive.ts +++ b/src/prompts/sdk/publish/interactive.ts @@ -1,7 +1,7 @@ import { confirm, isCancel, log, select, text } from '@clack/prompts'; import { Result } from 'neverthrow'; import { format as f } from '../../../prompts/format.js'; -import { noteWrapped, withSpinner } from '../../prompt.js'; +import { withSpinner } from '../../prompt.js'; import { DirectoryPath } from '../../../types/file/directoryPath.js'; import { ServiceError } from '../../../infrastructure/service-error.js'; import { @@ -194,10 +194,4 @@ export class SdkPublishInteractivePrompts { 'Version tags will not be created in your Git repository because you have opted to publish Source Code only.' ); } - - public sdkPublishingInProgress(publishingLogUrl: string) { - const message = `To view the status of publishing, please visit: -${f.link(publishingLogUrl)}`; - noteWrapped(message, 'Next Steps'); - } } diff --git a/src/prompts/sdk/publish/non-interactive.ts b/src/prompts/sdk/publish/non-interactive.ts index 8fe5763e..d25c83c0 100644 --- a/src/prompts/sdk/publish/non-interactive.ts +++ b/src/prompts/sdk/publish/non-interactive.ts @@ -1,15 +1,12 @@ -import { log, spinner } from '@clack/prompts'; +import { log } from '@clack/prompts'; import { Result } from 'neverthrow'; import { PublishType } from '../../../types/publish-api/publishing-profile-item.js'; -import { SemVersion } from '../../../types/publish/version.js'; import { format as f } from '../../../prompts/format.js'; import { DirectoryPath } from '../../../types/file/directoryPath.js'; import { ServiceError } from '../../../infrastructure/service-error.js'; -import { noteWrapped, withSpinner } from '../../prompt.js'; +import { withSpinner } from '../../prompt.js'; import { PublishingProfileItem } from '../../../types/publish-api/publishing-profile-item.js'; -import { PublishLogItem } from '../../../types/publish-api/publish-log.js'; import { ProfileId } from '../../../types/publish/profile-id.js'; -import { PublishingProfile } from '../../../types/publish/publishing-profile.js'; import { Language } from '../../../types/sdk/generate.js'; export class SdkPublishNonInteractivePrompts { @@ -83,69 +80,4 @@ export class SdkPublishNonInteractivePrompts { 'Version tags will not be created in your Git repository because you have opted to publish Source Code only.' ); } - - 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 postPublishingMessage(publishingLogUrl: string) { - const message = `To view publishing logs, please visit: -${f.link(publishingLogUrl)}`; - noteWrapped(message, 'Next Steps'); - } - - public async pollPublishingStatus( - getSdkPublishingLogFn: () => Promise> - ): Promise { - const TERMINAL_STATES = new Set(['Succeeded', 'Failed', 'Exception', 'InternalError']); - const POLL_INTERVAL_MS = 10000; // poll after every 10 seconds. - const spin = spinner(); - - spin.start('Waiting for publishing status...'); - - while (true) { - const publishingLogResult = await getSdkPublishingLogFn(); - - if (publishingLogResult.isErr()) { - spin.stop('Failed to fetch publishing status.', 1); - return false; - } - - const { events } = publishingLogResult.value; - const executionCompleted = events.every((event) => TERMINAL_STATES.has(event.eventType)); - const statusMessage = [...events] - .sort((a, b) => (a.publishType === 'SourceCode' ? -1 : b.publishType === 'SourceCode' ? 1 : 0)) - .map((event) => { - const target = event.publishType === 'SourceCode' ? 'Source Code' : 'Package'; - const eventLabels: Record = { - Queued: 'Queued', - InProgress: 'In Progress', - Succeeded: 'Published' - }; - const label = eventLabels[event.eventType] ?? 'Failed'; - return `${target}: [${label}]`; - }) - .join(' | '); - - if (executionCompleted) { - const isExecutionSuccessful = events.every((event) => event.eventType === 'Succeeded'); - spin.stop(statusMessage, isExecutionSuccessful ? 0 : 1); - return isExecutionSuccessful; - } - - spin.message(statusMessage); - await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS)); - } - } }