Repository navigation
feat(sdk): accept --auth-key in sdk publish #470
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,7 +18,11 @@ export class SdkPublishNonInteractiveAction { | |
| private readonly prompts: SdkPublishNonInteractivePrompts = new SdkPublishNonInteractivePrompts(); | ||
| private readonly publishingApiService: PublishingApiService = new PublishingApiService(); | ||
|
|
||
| public constructor(private readonly configDir: DirectoryPath, private readonly commandMetadata: CommandMetadata) {} | ||
| public constructor( | ||
| private readonly configDir: DirectoryPath, | ||
| private readonly commandMetadata: CommandMetadata, | ||
| private readonly authKey: string | null = null | ||
| ) {} | ||
|
|
||
| public readonly execute = async ( | ||
| project: ProjectContext, | ||
|
|
@@ -63,7 +67,7 @@ export class SdkPublishNonInteractiveAction { | |
| } | ||
|
|
||
| const publishingProfilesResponse = await this.prompts.getPublishingProfiles( | ||
| this.publishingApiService.getPublishingProfiles(this.configDir, this.commandMetadata.shell) | ||
| this.publishingApiService.getPublishingProfiles(this.configDir, this.commandMetadata.shell, this.authKey) | ||
| ); | ||
| if (publishingProfilesResponse.isErr()) { | ||
| this.prompts.getPublishingProfilesServiceError(publishingProfilesResponse.error); | ||
|
|
@@ -108,7 +112,7 @@ export class SdkPublishNonInteractiveAction { | |
| stability: stability.chosenLevel() | ||
| }); | ||
| // A run with flags is the CI flow: it never waits on a question, whether a terminal is attached or not, and always keeps the SDK's edits. | ||
| const publishResult = await new SdkPublishAction(this.configDir, this.commandMetadata).execute( | ||
| const publishResult = await new SdkPublishAction(this.configDir, this.commandMetadata, this.authKey).execute( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nothing tests that |
||
| project, | ||
| sdkDirectory, | ||
| language, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,4 @@ | ||
| import { Command, Flags } from '@oclif/core'; | ||
| import { Command, Flags, Interfaces } from '@oclif/core'; | ||
| import { DirectoryPath } from '../../types/file/directoryPath.js'; | ||
| import { FlagsProvider } from '../../types/flags-provider.js'; | ||
| import { Language, Stability } from '../../types/sdk/generate.js'; | ||
|
|
@@ -15,7 +15,7 @@ import { SdkPublishNonInteractiveAction } from '../../actions/sdk/publish/non-in | |
| export default class SdkPublish extends Command { | ||
| static readonly summary = 'Generate and publish an SDK to a package registry and/or source repository'; | ||
|
|
||
| static readonly description = `Generate and publish an SDK using a publishing profile configured in the APIMatic App. Requires an input directory containing the API specification. Run it with no flags, or with only --git-init, for guided mode, or pass flags for CI/CD. When you pass flags, --profile-id, --language, --version, and --publish-type are required. | ||
| static readonly description = `Generate and publish an SDK using a publishing profile configured in the APIMatic App. Requires an input directory containing the API specification. Run it with no flags, or with only --git-init or --auth-key, for guided mode, or pass flags for CI/CD. When you pass flags, --profile-id, --language, --version, and --publish-type are required. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "with only --git-init or --auth-key" reads as one or the other, but both together are guided too (the PR body says "and/or"). Maybe "with only --git-init and/or --auth-key", here and in apimatic-docs#884. |
||
| Your changes to the SDK are merged before it is published, as sdk generate does with git 2.38 or later: with flags, always; in guided mode, add --git-init. Nothing is published while conflicts are left to resolve.`; | ||
|
|
||
| static readonly cmdTxt = format.cmd('apimatic', 'sdk', 'publish'); | ||
|
|
@@ -56,7 +56,8 @@ Your changes to the SDK are merged before it is published, as sdk generate does | |
| default: false, | ||
| description: | ||
| "In guided mode, make the SDK directory a git repository when it isn't one of its own, so your changes to the SDK are kept. With flags, sdk publish always does this." | ||
| }) | ||
| }), | ||
| ...FlagsProvider.authKey | ||
| }; | ||
|
|
||
| static examples = [ | ||
|
|
@@ -80,9 +81,9 @@ Your changes to the SDK are merged before it is published, as sdk generate does | |
| )}` | ||
| ]; | ||
|
|
||
| /** Guided mode is asked for by no flags, or `--git-init` alone. */ | ||
| static isGuided(argv: readonly string[]): boolean { | ||
| return argv.every((arg) => arg === '--git-init'); | ||
| /** Guided mode is asked for by no flags, or only `--git-init` and `--auth-key`. */ | ||
| static isGuided(raw: Interfaces.ParserOutput['raw']): boolean { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The list of flags that don't count against guided mode will grow with every cross-cutting flag. It already leaves out |
||
| return raw.every((token) => token.type === 'flag' && (token.flag === 'git-init' || token.flag === 'auth-key')); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| } | ||
|
|
||
| async run() { | ||
|
|
@@ -97,12 +98,14 @@ Your changes to the SDK are merged before it is published, as sdk generate does | |
| 'publish-type': publishType, | ||
| 'dry-run': dryRun, | ||
| stability, | ||
| 'git-init': gitInit | ||
| } | ||
| 'git-init': gitInit, | ||
| 'auth-key': authKey | ||
| }, | ||
| raw | ||
| } = await this.parse(SdkPublish); | ||
|
|
||
| const publishTypes = [...new Set(publishType)] as PublishType[]; | ||
| const interactive = SdkPublish.isGuided(this.argv); | ||
| const interactive = SdkPublish.isGuided(raw); | ||
| const commandMetadata: CommandMetadata = { | ||
| commandName: SdkPublish.id, | ||
| shell: this.config.shell | ||
|
|
@@ -134,12 +137,12 @@ Your changes to the SDK are merged before it is published, as sdk generate does | |
| const sdkDirectory = destination === undefined ? undefined : DirectoryPath.fromUserInput(destination); | ||
| intro('Publish SDK'); | ||
| const result = interactive | ||
| ? await new SdkPublishInteractiveAction(configDir, commandMetadata).execute( | ||
| ? await new SdkPublishInteractiveAction(configDir, commandMetadata, authKey).execute( | ||
| DirectoryPath.createInput(input), | ||
| gitInit, | ||
| onPublishSdkError | ||
| ) | ||
| : await new SdkPublishNonInteractiveAction(configDir, commandMetadata).execute( | ||
| : await new SdkPublishNonInteractiveAction(configDir, commandMetadata, authKey).execute( | ||
| project, | ||
| sdkDirectory, | ||
| language as Language, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -25,8 +25,7 @@ export class PublishingApiService { | |
| authKey: string | null = null | ||
| ): Promise<Result<PublishingProfileItem[], ServiceError>> { | ||
| const authInfo: AuthInfo | null = await getAuthInfo(configDir.toString()); | ||
| // `auth logout` blanks config.json rather than deleting it, so a logged-out user | ||
| // still has a non-null AuthInfo with an empty key — check the key, not the object. | ||
| // `auth logout` blanks config.json rather than deleting it, so check the key, not the AuthInfo. | ||
| const token = authKey || authInfo?.authKey; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This resolve-and-check block now appears in all three methods, and |
||
| if (!token) { | ||
| return err(ServiceError.unauthorizedWithHint(null)); | ||
|
|
@@ -51,25 +50,25 @@ export class PublishingApiService { | |
| packageVersion: SemVersion, | ||
| publishType: PublishType[], | ||
| configDir: DirectoryPath, | ||
| shell: string | ||
| shell: string, | ||
| authKey: string | null = null | ||
| ): Promise<Result<PublishingInfo, ServiceError>> { | ||
| const authInfo: AuthInfo | null = await getAuthInfo(configDir.toString()); | ||
| // `auth logout` blanks config.json rather than deleting it, so a logged-out user | ||
| // still has a non-null AuthInfo with an empty key — check the key, not the object. | ||
| if (!authInfo?.authKey) { | ||
| // `auth logout` blanks config.json rather than deleting it, so check the key, not the AuthInfo. | ||
| const token = authKey || authInfo?.authKey; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. An empty key ( |
||
| if (!token) { | ||
| return err(ServiceError.unauthorizedWithHint(null)); | ||
| } | ||
| const sdkFileStream = await this.fileService.getStream(sdkFilePath); | ||
|
|
||
| try { | ||
| const token = authInfo.authKey; | ||
| const formData = new FormData(); | ||
| formData.append('file', sdkFileStream); | ||
| formData.append('packageVersion', packageVersion.toString()); | ||
|
|
||
| // If both package and source code publishing are not selected, publishType array will contain only one of them, so we need to specify which one to the API. | ||
| // If both package and source code publishing are not selected, publishType array will contain only one of them, so we need to specify which one to the API. | ||
| // If both are selected, we will omit this field as API defaults to both types being selected. | ||
| if (publishType.length !== 2) { | ||
| if (publishType.length !== 2) { | ||
| formData.append('publishType', publishType[0]); | ||
| } | ||
|
|
||
|
|
@@ -109,17 +108,17 @@ export class PublishingApiService { | |
| public async getSdkPublishingLog( | ||
| publishingLogId: string, | ||
| configDir: DirectoryPath, | ||
| shell: string | ||
| shell: string, | ||
| authKey: string | null = null | ||
| ): Promise<Result<PublishLogItem, ServiceError>> { | ||
| const authInfo: AuthInfo | null = await getAuthInfo(configDir.toString()); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor: |
||
| // `auth logout` blanks config.json rather than deleting it, so a logged-out user | ||
| // still has a non-null AuthInfo with an empty key — check the key, not the object. | ||
| if (!authInfo?.authKey) { | ||
| // `auth logout` blanks config.json rather than deleting it, so check the key, not the AuthInfo. | ||
| const token = authKey || authInfo?.authKey; | ||
| if (!token) { | ||
| return err(ServiceError.unauthorizedWithHint(null)); | ||
| } | ||
|
|
||
| try { | ||
| const token = authInfo.authKey; | ||
| const response = await this.axiosInstance(shell, token).get(`/publish-logs/${publishingLogId}`); | ||
|
|
||
| if (response.status === 200) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,6 +19,7 @@ import { FileProblem } from '../../../src/types/file/file-problem.js'; | |
| import { GitContext } from '../../../src/types/git-context.js'; | ||
| import { ProjectContext } from '../../../src/types/project-context.js'; | ||
| import { PublishingProfileItem, PublishType } from '../../../src/types/publish-api/publishing-profile-item.js'; | ||
| import { PublishLogItem } from '../../../src/types/publish-api/publish-log.js'; | ||
| import { ProfileId } from '../../../src/types/publish/profile-id.js'; | ||
| import { PublishingProfile } from '../../../src/types/publish/publishing-profile.js'; | ||
| import { SemVersion } from '../../../src/types/publish/version.js'; | ||
|
|
@@ -34,6 +35,7 @@ const COMMAND_METADATA: CommandMetadata = { commandName: 'sdk publish', shell: ' | |
| describe('SdkPublishAction', () => { | ||
| let root: string; | ||
| let generates: (files: Files) => Promise<void>; | ||
| let generateSdk: sinon.SinonStub; | ||
| let onPublishSdkError: sinon.SinonStub; | ||
| let restoreConfig: () => void; | ||
|
|
||
|
|
@@ -63,7 +65,7 @@ describe('SdkPublishAction', () => { | |
|
|
||
| beforeEach(async () => { | ||
| root = fs.mkdtempSync(path.join(os.tmpdir(), 'sdk-publish-')); | ||
| ({ generates } = stubSdkGeneration(root)); | ||
| ({ generates, generateSdk } = stubSdkGeneration(root)); | ||
| await generates({ 'README.md': '# sdk' }); | ||
| onPublishSdkError = sinon.stub(); | ||
| restoreConfig = emptyGlobalGitConfig(root); | ||
|
|
@@ -115,6 +117,43 @@ describe('SdkPublishAction', () => { | |
| expect(fs.existsSync(path.join(sdk().toString(), 'node_modules', 'dep.js'))).to.be.true; | ||
| }); | ||
|
|
||
| it('uses the auth key it was given to generate, upload and follow the publish', async () => { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: this repeats three of |
||
| quietGeneration(); | ||
| sinon.stub(SdkPublishPrompts.prototype, 'publishSdk').callsFake((fn) => fn); | ||
| sinon.stub(SdkPublishPrompts.prototype, 'publishingRunningNotice'); | ||
| sinon.stub(SdkPublishPrompts.prototype, 'publishingLogsMessage'); | ||
| sinon.stub(SdkPublishPrompts.prototype, 'pollPublishingStatus').callsFake(async (poll) => { | ||
| await poll(); | ||
| return 'succeeded'; | ||
| }); | ||
| const publishSdkPackage = uploads([]); | ||
| const getSdkPublishingLog = sinon | ||
| .stub(PublishingApiService.prototype, 'getSdkPublishingLog') | ||
| .resolves(ok({} as PublishLogItem)); | ||
|
|
||
| const result = await new SdkPublishAction(new DirectoryPath(root), COMMAND_METADATA, 'flag-key').execute( | ||
| ProjectContext.in(new DirectoryPath(root)), | ||
| sdk(), | ||
| Language.PYTHON, | ||
| Stability.STABLE, | ||
| [PublishType.PackagePublishing], | ||
| true, | ||
| false, | ||
| true, | ||
| ProfileId.tryCreate('profile')._unsafeUnwrap(), | ||
| SemVersion.tryCreate('1.0.0')._unsafeUnwrap(), | ||
| withoutPackageSettings, | ||
| false, | ||
| 'summary', | ||
| onPublishSdkError | ||
| ); | ||
|
|
||
| expect(result.isSuccess()).to.be.true; | ||
| expect(generateSdk.firstCall.args[5]).to.equal('flag-key'); | ||
| expect(publishSdkPackage.firstCall.args[7]).to.equal('flag-key'); | ||
| expect(getSdkPublishingLog.firstCall.args[3]).to.equal('flag-key'); | ||
| }); | ||
|
|
||
| it('generates into the SDK directory on a dry run and opens it, uploading nothing', async () => { | ||
| quietGeneration(); | ||
| const dryRunNotice = sinon.stub(SdkPublishPrompts.prototype, 'dryRunNotice'); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Optional: the raw key now travels through three action constructors and three service methods next to
configDir, and each service decides flag-or-login on its own. Resolving it once in the command and passing the result down would remove the repeated lookups.