Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 35 additions & 2 deletions src/actions/sdk/publish.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ export class SdkPublishAction {
publishingProfile: PublishingProfile,
dryRun: boolean,
onPublishSdkError: (errorMessage: string) => void
): Promise<ActionResult<PublishingInfo>> => {
return await withDirPath(async (tempDirectory) => {
): Promise<ActionResult> => {
const publishResult = await withDirPath(async (tempDirectory): Promise<ActionResult<PublishingInfo>> => {
const packageConfigurationData = publishingProfile.getPackageConfigurationDataForLanguage(language);
let packageSettingsDirectory: DirectoryPath | undefined;

Expand Down Expand Up @@ -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) {
Comment thread
mrafnadeem-apimatic marked this conversation as resolved.
return ActionResult.success();
}

const publishingInfo = publishResult.getValue();
this.prompts.publishingRunningNotice(publishingProfile, language, semVersion, publishType);
Comment thread
mrafnadeem-apimatic marked this conversation as resolved.
this.prompts.publishingLogsMessage(publishingInfo.publishingLogUrl);
Comment thread
mrafnadeem-apimatic marked this conversation as resolved.

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:
Comment thread
mrafnadeem-apimatic marked this conversation as resolved.
return ActionResult.failed();
}
};
}
1 change: 0 additions & 1 deletion src/actions/sdk/publish/interactive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,6 @@ export class SdkPublishInteractiveAction {
return ActionResult.cancelled();
}

this.prompts.sdkPublishingInProgress(publishResult.getValue().publishingLogUrl);
return ActionResult.success();
};

Expand Down
22 changes: 0 additions & 22 deletions src/actions/sdk/publish/non-interactive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
};
}
137 changes: 111 additions & 26 deletions src/prompts/sdk/publish.ts
Original file line number Diff line number Diff line change
@@ -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<Result<PublishingInfo, ServiceError>>) {
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';
Comment thread
mrafnadeem-apimatic marked this conversation as resolved.
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<Result<PublishingInfo, ServiceError>>) {
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 {
Comment thread
mrafnadeem-apimatic marked this conversation as resolved.
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))

Check warning on line 38 in src/prompts/sdk/publish.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Extract this nested ternary operation into an independent statement.

See more on https://sonarcloud.io/project/issues?id=apimatic_apimatic-cli&issues=AZ_wEcZSzmscKD-OqshH&open=AZ_wEcZSzmscKD-OqshH&pullRequest=315
Comment thread
mrafnadeem-apimatic marked this conversation as resolved.
.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(
Comment thread
mrafnadeem-apimatic marked this conversation as resolved.
getSdkPublishingLogFn: () => Promise<Result<PublishLogItem, ServiceError>>
): Promise<PublishingOutcome> {
const TERMINAL_STATES = new Set(['Succeeded', 'Failed', 'Exception', 'InternalError']);
const POLL_INTERVAL_MS = 10000; // poll after every 10 seconds.
Comment thread
mrafnadeem-apimatic marked this conversation as resolved.

let abortWait: (() => void) | undefined;
Comment thread
mrafnadeem-apimatic marked this conversation as resolved.
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) {
Comment thread
mrafnadeem-apimatic marked this conversation as resolved.
const publishingLogResult = await getSdkPublishingLogFn();

if (publishingLogResult.isErr()) {
Comment thread
mrafnadeem-apimatic marked this conversation as resolved.
spin.stop('Failed to fetch publishing status.', 1);
return 'failed';
}

const { events } = publishingLogResult.value;
const executionCompleted = events.every((event) => TERMINAL_STATES.has(event.eventType));
Comment thread
mrafnadeem-apimatic marked this conversation as resolved.
const statusMessage = [...events]
.sort((a, b) => (a.publishType === 'SourceCode' ? -1 : b.publishType === 'SourceCode' ? 1 : 0))

Check warning on line 79 in src/prompts/sdk/publish.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Extract this nested ternary operation into an independent statement.

See more on https://sonarcloud.io/project/issues?id=apimatic_apimatic-cli&issues=AZ_wEcZSzmscKD-OqshI&open=AZ_wEcZSzmscKD-OqshI&pullRequest=315
.map((event) => {
const target = event.publishType === 'SourceCode' ? 'Source Code' : 'Package';
const eventLabels: Record<string, string> = {
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);
Comment thread
mrafnadeem-apimatic marked this conversation as resolved.
return isExecutionSuccessful ? 'succeeded' : 'failed';
}

spin.message(statusMessage);
await new Promise<void>((resolve) => {
const timer = setTimeout(resolve, POLL_INTERVAL_MS);
abortWait = () => {
clearTimeout(timer);
resolve();
};
});
abortWait = undefined;
Comment thread
mrafnadeem-apimatic marked this conversation as resolved.
}

return 'cancelled';
}
}
8 changes: 1 addition & 7 deletions src/prompts/sdk/publish/interactive.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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');
}
}
72 changes: 2 additions & 70 deletions src/prompts/sdk/publish/non-interactive.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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<Result<PublishLogItem, ServiceError>>
): Promise<boolean> {
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<string, string> = {
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));
}
}
}
Loading