-
Notifications
You must be signed in to change notification settings - Fork 226
Allow non-interactive app init
#6640
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
Open
nickwesselman
wants to merge
9
commits into
main
Choose a base branch
from
allow_noninteractive_app_init
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
771537c
add organization-id flag to suppress organization selection
nickwesselman 2e147ad
ensure --name flag suppresses interactive name prompt
nickwesselman d1abc81
add unit tests for init command
nickwesselman 4d8d03a
remove unused mocks
nickwesselman 5a50e1b
added changeset
nickwesselman 25d84c9
update dev docs
nickwesselman b521fdb
only apply SHOPIFY_FLAG_CLIENT_ID to acceptance tests
nickwesselman 0157269
improve org error message
nickwesselman c717875
Use orgFromId to validate organization
nickwesselman File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| '@shopify/app': patch | ||
| --- | ||
|
|
||
| Enable non-interactive `app init` via a new `--organization-id` flag and not prompting to link to an existing app if `--name` is provided. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,156 @@ | ||
| import Init from './init.js' | ||
| import initPrompt from '../../prompts/init/init.js' | ||
| import initService from '../../services/init/init.js' | ||
| import {selectDeveloperPlatformClient} from '../../utilities/developer-platform-client.js' | ||
| import {selectOrg} from '../../services/context.js' | ||
| import {appNamePrompt, createAsNewAppPrompt} from '../../prompts/dev.js' | ||
| import {validateFlavorValue, validateTemplateValue} from '../../services/init/validate.js' | ||
| import {testAppLinked, testDeveloperPlatformClient, testOrganization} from '../../models/app/app.test-data.js' | ||
| import {describe, expect, test, vi} from 'vitest' | ||
| import {mockAndCaptureOutput} from '@shopify/cli-kit/node/testing/output' | ||
| import {generateRandomNameForSubdirectory} from '@shopify/cli-kit/node/fs' | ||
| import {inferPackageManager} from '@shopify/cli-kit/node/node-package-manager' | ||
|
|
||
| vi.mock('../../prompts/init/init.js') | ||
| vi.mock('../../services/init/init.js') | ||
| vi.mock('../../utilities/developer-platform-client.js') | ||
| vi.mock('../../services/context.js') | ||
| vi.mock('../../prompts/dev.js') | ||
| vi.mock('../../services/init/validate.js') | ||
| vi.mock('@shopify/cli-kit/node/fs') | ||
| vi.mock('@shopify/cli-kit/node/node-package-manager') | ||
|
|
||
| describe('Init command', () => { | ||
| test('runs init command with default flags', async () => { | ||
| // Given | ||
| const mockOrganization = testOrganization() | ||
| const mockDeveloperPlatformClient = testDeveloperPlatformClient() | ||
| const mockApp = testAppLinked() | ||
|
|
||
| mockAndCaptureOutput() | ||
| vi.mocked(validateTemplateValue).mockReturnValue(undefined) | ||
| vi.mocked(validateFlavorValue).mockReturnValue(undefined) | ||
| vi.mocked(inferPackageManager).mockReturnValue('npm') | ||
| vi.mocked(generateRandomNameForSubdirectory).mockResolvedValue('test-app') | ||
| vi.mocked(selectDeveloperPlatformClient).mockReturnValue(mockDeveloperPlatformClient) | ||
| vi.mocked(selectOrg).mockResolvedValue(mockOrganization) | ||
|
|
||
| // Mock the orgAndApps method on the developer platform client | ||
| vi.mocked(mockDeveloperPlatformClient.orgAndApps).mockResolvedValue({ | ||
| organization: mockOrganization, | ||
| apps: [], | ||
| hasMorePages: false, | ||
| }) | ||
|
|
||
| vi.mocked(initPrompt).mockResolvedValue({ | ||
| template: 'https://github.com/Shopify/shopify-app-template-remix', | ||
| templateType: 'remix', | ||
| globalCLIResult: {install: false, alreadyInstalled: false}, | ||
| }) | ||
| vi.mocked(createAsNewAppPrompt).mockResolvedValue(true) | ||
| vi.mocked(appNamePrompt).mockResolvedValue('test-app') | ||
| vi.mocked(initService).mockResolvedValue({app: mockApp}) | ||
|
|
||
| // When | ||
| await Init.run([]) | ||
|
|
||
| // Then | ||
| expect(initService).toHaveBeenCalledWith( | ||
| expect.objectContaining({ | ||
| name: 'test-app', | ||
| packageManager: 'npm', | ||
| }), | ||
| ) | ||
| }) | ||
|
|
||
| test('runs init command without prompts when organization-id, name, and template flags are provided', async () => { | ||
| // Given | ||
| const mockOrganization = testOrganization() | ||
| const mockDeveloperPlatformClient = testDeveloperPlatformClient() | ||
| const mockApp = testAppLinked() | ||
|
|
||
| mockAndCaptureOutput() | ||
| vi.mocked(validateTemplateValue).mockReturnValue(undefined) | ||
| vi.mocked(validateFlavorValue).mockReturnValue(undefined) | ||
| vi.mocked(inferPackageManager).mockReturnValue('npm') | ||
| vi.mocked(selectDeveloperPlatformClient).mockReturnValue(mockDeveloperPlatformClient) | ||
|
|
||
| // Mock orgFromId to return the organization | ||
| vi.mocked(mockDeveloperPlatformClient.orgFromId).mockResolvedValue(mockOrganization) | ||
|
|
||
| // Mock the orgAndApps method on the developer platform client | ||
| vi.mocked(mockDeveloperPlatformClient.orgAndApps).mockResolvedValue({ | ||
| organization: mockOrganization, | ||
| apps: [], | ||
| hasMorePages: false, | ||
| }) | ||
|
|
||
| vi.mocked(initPrompt).mockResolvedValue({ | ||
| template: 'https://github.com/Shopify/shopify-app-template-remix', | ||
| templateType: 'remix', | ||
| globalCLIResult: {install: false, alreadyInstalled: false}, | ||
| }) | ||
| vi.mocked(initService).mockResolvedValue({app: mockApp}) | ||
|
|
||
| // When | ||
| await Init.run(['--organization-id', mockOrganization.id, '--name', 'my-app', '--template', 'remix']) | ||
|
|
||
| // Then | ||
| // Verify that prompt functions were NOT called | ||
| // Any other interactive prompts would also cause the test to fail with an AbortError | ||
| expect(selectOrg).not.toHaveBeenCalled() | ||
| expect(createAsNewAppPrompt).not.toHaveBeenCalled() | ||
| expect(appNamePrompt).not.toHaveBeenCalled() | ||
|
|
||
| // Verify the command completed successfully | ||
| expect(initService).toHaveBeenCalledWith( | ||
| expect.objectContaining({ | ||
| name: 'my-app', | ||
| packageManager: 'npm', | ||
| template: 'https://github.com/Shopify/shopify-app-template-remix', | ||
| }), | ||
| ) | ||
| }) | ||
|
|
||
| test('fails with clear error message when invalid organization-id is provided', async () => { | ||
| // Given | ||
| const validOrg = testOrganization() | ||
| const mockDeveloperPlatformClient = testDeveloperPlatformClient() | ||
|
|
||
| // Suppress stderr output for this error test | ||
| const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) | ||
|
|
||
| try { | ||
| const outputMock = mockAndCaptureOutput() | ||
| vi.mocked(validateTemplateValue).mockReturnValue(undefined) | ||
| vi.mocked(validateFlavorValue).mockReturnValue(undefined) | ||
| vi.mocked(inferPackageManager).mockReturnValue('npm') | ||
| vi.mocked(selectDeveloperPlatformClient).mockReturnValue(mockDeveloperPlatformClient) | ||
|
|
||
| // Mock orgFromId to return undefined for invalid organization | ||
| vi.mocked(mockDeveloperPlatformClient.orgFromId).mockResolvedValue(undefined) | ||
|
|
||
| vi.mocked(initPrompt).mockResolvedValue({ | ||
| template: 'https://github.com/Shopify/shopify-app-template-remix', | ||
| templateType: 'remix', | ||
| globalCLIResult: {install: false, alreadyInstalled: false}, | ||
| }) | ||
|
|
||
| // When/Then | ||
| // The command throws an AbortError which is caught by oclif's error handler | ||
| // This causes process.exit(1) which vitest intercepts | ||
| await expect( | ||
| Init.run(['--organization-id', 'invalid-org-id', '--name', 'my-app', '--template', 'remix']), | ||
| ).rejects.toThrow('process.exit unexpectedly called with "1"') | ||
|
|
||
| // Verify the error message was displayed | ||
| expect(outputMock.error()).toContain('Organization with ID invalid-org-id not found') | ||
|
|
||
| // Verify initService was never called since validation failed | ||
| expect(initService).not.toHaveBeenCalled() | ||
| } finally { | ||
| // Always restore console.error, even if the test fails | ||
| consoleErrorSpy.mockRestore() | ||
| } | ||
| }) | ||
| }) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
Nit: I did not get this helpful message when sending a non-integer as a parameter -- just got a bare GraphQL error. Perhaps we could use typing or otherwise be more defensive about input?