-
Notifications
You must be signed in to change notification settings - Fork 66
feat: Add hs app migrate
command
#1406
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
Merged
Merged
Changes from all commits
Commits
Show all changes
42 commits
Select commit
Hold shift + click to select a range
193c571
wip
joe-yeager 307a833
wip
joe-yeager 2d96fbb
Fake it
joe-yeager 503b2c8
wip
joe-yeager 99f343e
Refactor
joe-yeager b9e1611
Add additional reasons
joe-yeager 175e36c
Review copy with Jono
joe-yeager e7350b4
test
joe-yeager 0d20207
Integration tweaks
joe-yeager 143ad49
v7.2.3-experimental.0
joe-yeager ac26a4b
Add logs displaying what will be migrated, prompt to proceed
joe-yeager 009fe74
Remove duplicate tracking event, add shell for hs project migrate
joe-yeager 84013dd
Merge branch 'main' of github.com:HubSpot/hubspot-cli into jy/migrati…
joe-yeager 3f556b3
Clean up
joe-yeager 78cb580
Clean up, add tests
joe-yeager 79ee7c8
Implement polling
joe-yeager 54ca912
Merge branch 'main' of github.com:HubSpot/hubspot-cli into jy/migrati…
joe-yeager 80e3001
Udpate after LDL release
joe-yeager 87fd9d5
Add defaultAnswer for dest
joe-yeager 39393cf
clean up
joe-yeager 505df3d
Update LDL version
joe-yeager bbbed83
Fix wierd flag behavior
joe-yeager 4907c78
Remove async and promise wrapper
joe-yeager 8bb55a6
Merge branch 'main' of github.com:HubSpot/hubspot-cli into jy/migrati…
joe-yeager bfafd18
Feedback
joe-yeager 9ce5273
feedback
joe-yeager adfcc01
Get it working with new endpoints
joe-yeager 91b557e
Use experimental release
joe-yeager cd6fe01
Remove console.logs
joe-yeager 1e5b7c0
Merge branch 'main' of github.com:HubSpot/hubspot-cli into jy/migrati…
joe-yeager 2fcbaa9
Fixes
joe-yeager d1b3243
Undo
joe-yeager 110baf1
Inline i18nkey
joe-yeager 210cb5f
clean up
joe-yeager 223f554
PR feedback, move stuff around, add API calls to CLI
joe-yeager 6daa0df
Fix bad import
joe-yeager 8ac0831
Remove migration spinner
joe-yeager 6bff187
Merge branch 'main' of github.com:HubSpot/hubspot-cli into jy/migrati…
joe-yeager 9d60f24
Feedback from demo
joe-yeager 04e54f6
fix bug with filtering apps
joe-yeager e2d634c
Merge branch 'main' of github.com:HubSpot/hubspot-cli into jy/migrati…
joe-yeager 9c02037
Hide zip extraction logs
joe-yeager 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,190 @@ | ||
import { http } from '@hubspot/local-dev-lib/http'; | ||
import { MIGRATION_STATUS } from '@hubspot/local-dev-lib/types/Migration'; | ||
import { | ||
listAppsForMigration, | ||
initializeMigration, | ||
continueMigration, | ||
checkMigrationStatusV2, | ||
ListAppsResponse, | ||
MigrationStatus, | ||
} from '../migrate'; | ||
|
||
jest.mock('@hubspot/local-dev-lib/http'); | ||
|
||
const httpMock = http as jest.Mocked<typeof http>; | ||
|
||
describe('api/migrate', () => { | ||
const mockAccountId = 12345; | ||
const mockPortalId = 12345; | ||
const mockAppId = 67890; | ||
const mockMigrationId = 54321; | ||
const mockPlatformVersion = '2025.2'; | ||
const mockProjectName = 'Test Project'; | ||
const mockComponentUids = { 'component-1': 'uid-1', 'component-2': 'uid-2' }; | ||
|
||
describe('listAppsForMigration', () => { | ||
it('should call http.get with correct parameters', async () => { | ||
const mockResponse: ListAppsResponse = { | ||
migratableApps: [ | ||
{ | ||
appId: 1, | ||
appName: 'App 1', | ||
isMigratable: true, | ||
migrationComponents: [ | ||
{ id: 'comp1', componentType: 'type1', isSupported: true }, | ||
], | ||
}, | ||
], | ||
unmigratableApps: [ | ||
{ | ||
appId: 2, | ||
appName: 'App 2', | ||
isMigratable: false, | ||
unmigratableReason: 'UP_TO_DATE', | ||
migrationComponents: [ | ||
{ id: 'comp2', componentType: 'type2', isSupported: false }, | ||
], | ||
}, | ||
], | ||
}; | ||
|
||
// @ts-expect-error Mock | ||
httpMock.get.mockResolvedValue(mockResponse); | ||
|
||
const result = await listAppsForMigration(mockAccountId); | ||
|
||
expect(http.get).toHaveBeenCalledWith(mockAccountId, { | ||
url: 'dfs/migrations/v2/list-apps', | ||
}); | ||
expect(result).toEqual(mockResponse); | ||
}); | ||
}); | ||
|
||
describe('initializeMigration', () => { | ||
it('should call http.post with correct parameters', async () => { | ||
const mockResponse = { migrationId: mockMigrationId }; | ||
// @ts-expect-error Mock | ||
httpMock.post.mockResolvedValue(mockResponse); | ||
|
||
const result = await initializeMigration( | ||
mockAccountId, | ||
mockAppId, | ||
mockPlatformVersion | ||
); | ||
|
||
expect(http.post).toHaveBeenCalledWith(mockAccountId, { | ||
url: 'dfs/migrations/v2/migrations', | ||
data: { | ||
applicationId: mockAppId, | ||
platformVersion: 'V2025_2', | ||
}, | ||
}); | ||
expect(result).toEqual(mockResponse); | ||
}); | ||
}); | ||
|
||
describe('continueMigration', () => { | ||
it('should call http.post with correct parameters', async () => { | ||
const mockResponse = { migrationId: mockMigrationId }; | ||
// @ts-expect-error Mock | ||
httpMock.post.mockResolvedValue(mockResponse); | ||
|
||
const result = await continueMigration( | ||
mockPortalId, | ||
mockMigrationId, | ||
mockComponentUids, | ||
mockProjectName | ||
); | ||
|
||
expect(http.post).toHaveBeenCalledWith(mockPortalId, { | ||
url: 'dfs/migrations/v2/migrations/continue', | ||
data: { | ||
migrationId: mockMigrationId, | ||
projectName: mockProjectName, | ||
componentUids: mockComponentUids, | ||
}, | ||
}); | ||
expect(result).toEqual(mockResponse); | ||
}); | ||
}); | ||
|
||
describe('checkMigrationStatusV2', () => { | ||
it('should call http.get with correct parameters for in-progress status', async () => { | ||
const mockResponse: MigrationStatus = { | ||
id: mockMigrationId, | ||
status: MIGRATION_STATUS.IN_PROGRESS, | ||
}; | ||
// @ts-expect-error Mock | ||
httpMock.get.mockResolvedValue(mockResponse); | ||
|
||
const result = await checkMigrationStatusV2( | ||
mockAccountId, | ||
mockMigrationId | ||
); | ||
|
||
expect(http.get).toHaveBeenCalledWith(mockAccountId, { | ||
url: `dfs/migrations/v2/migrations/${mockMigrationId}/status`, | ||
}); | ||
expect(result).toEqual(mockResponse); | ||
}); | ||
|
||
it('should handle input required status', async () => { | ||
const mockResponse: MigrationStatus = { | ||
id: mockMigrationId, | ||
status: MIGRATION_STATUS.INPUT_REQUIRED, | ||
componentsRequiringUids: { | ||
'component-1': { | ||
componentType: 'type1', | ||
componentHint: 'hint1', | ||
}, | ||
}, | ||
}; | ||
// @ts-expect-error Mock | ||
httpMock.get.mockResolvedValue(mockResponse); | ||
|
||
const result = await checkMigrationStatusV2( | ||
mockAccountId, | ||
mockMigrationId | ||
); | ||
|
||
expect(result).toEqual(mockResponse); | ||
}); | ||
|
||
it('should handle success status', async () => { | ||
const mockResponse: MigrationStatus = { | ||
id: mockMigrationId, | ||
status: MIGRATION_STATUS.SUCCESS, | ||
buildId: 98765, | ||
}; | ||
// @ts-expect-error Mock | ||
httpMock.get.mockResolvedValue(mockResponse); | ||
|
||
const result = await checkMigrationStatusV2( | ||
mockAccountId, | ||
mockMigrationId | ||
); | ||
|
||
expect(result).toEqual(mockResponse); | ||
}); | ||
|
||
it('should handle failure status', async () => { | ||
const mockResponse: MigrationStatus = { | ||
id: mockMigrationId, | ||
status: MIGRATION_STATUS.FAILURE, | ||
projectErrorsDetail: 'Error details', | ||
componentErrorDetails: { | ||
'component-1': 'Component error', | ||
}, | ||
}; | ||
// @ts-expect-error Mock | ||
httpMock.get.mockResolvedValue(mockResponse); | ||
|
||
const result = await checkMigrationStatusV2( | ||
mockAccountId, | ||
mockMigrationId | ||
); | ||
|
||
expect(result).toEqual(mockResponse); | ||
}); | ||
}); | ||
}); |
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,138 @@ | ||
import { HubSpotPromise } from '@hubspot/local-dev-lib/types/Http'; | ||
import { | ||
PLATFORM_VERSIONS, | ||
UNMIGRATABLE_REASONS, | ||
} from '@hubspot/local-dev-lib/constants/projects'; | ||
import { http } from '@hubspot/local-dev-lib/http'; | ||
import { MIGRATION_STATUS } from '@hubspot/local-dev-lib/types/Migration'; | ||
|
||
const MIGRATIONS_API_PATH_V2 = 'dfs/migrations/v2'; | ||
|
||
interface BaseMigrationApp { | ||
appId: number; | ||
appName: string; | ||
isMigratable: boolean; | ||
migrationComponents: ListAppsMigrationComponent[]; | ||
projectName?: string; | ||
} | ||
|
||
export interface MigratableApp extends BaseMigrationApp { | ||
isMigratable: true; | ||
} | ||
|
||
export interface UnmigratableApp extends BaseMigrationApp { | ||
isMigratable: false; | ||
unmigratableReason: keyof typeof UNMIGRATABLE_REASONS; | ||
} | ||
|
||
export type MigrationApp = MigratableApp | UnmigratableApp; | ||
|
||
export interface ListAppsResponse { | ||
migratableApps: MigratableApp[]; | ||
unmigratableApps: UnmigratableApp[]; | ||
} | ||
|
||
export interface InitializeMigrationResponse { | ||
migrationId: number; | ||
} | ||
|
||
export interface ListAppsMigrationComponent { | ||
id: string; | ||
componentType: string; | ||
isSupported: boolean; | ||
} | ||
|
||
export type ContinueMigrationResponse = { | ||
migrationId: number; | ||
}; | ||
|
||
export interface MigrationBaseStatus { | ||
id: number; | ||
} | ||
|
||
export interface MigrationInProgress extends MigrationBaseStatus { | ||
status: typeof MIGRATION_STATUS.IN_PROGRESS; | ||
} | ||
|
||
export interface MigrationInputRequired extends MigrationBaseStatus { | ||
status: typeof MIGRATION_STATUS.INPUT_REQUIRED; | ||
componentsRequiringUids: Record< | ||
string, | ||
{ | ||
componentType: string; | ||
componentHint: string; | ||
} | ||
>; | ||
} | ||
|
||
export interface MigrationSuccess extends MigrationBaseStatus { | ||
status: typeof MIGRATION_STATUS.SUCCESS; | ||
buildId: number; | ||
} | ||
|
||
export interface MigrationFailed extends MigrationBaseStatus { | ||
status: typeof MIGRATION_STATUS.FAILURE; | ||
projectErrorsDetail?: string; | ||
componentErrorDetails: Record<string, string>; | ||
} | ||
|
||
export type MigrationStatus = | ||
| MigrationInProgress | ||
| MigrationInputRequired | ||
| MigrationSuccess | ||
| MigrationFailed; | ||
|
||
export async function listAppsForMigration( | ||
accountId: number | ||
): HubSpotPromise<ListAppsResponse> { | ||
return http.get<ListAppsResponse>(accountId, { | ||
url: `${MIGRATIONS_API_PATH_V2}/list-apps`, | ||
}); | ||
} | ||
|
||
function mapPlatformVersionToEnum(platformVersion: string): string { | ||
if (platformVersion === PLATFORM_VERSIONS.unstable) { | ||
return PLATFORM_VERSIONS.unstable.toUpperCase(); | ||
} | ||
|
||
return `V${platformVersion.replace('.', '_')}`; | ||
} | ||
|
||
export async function initializeMigration( | ||
accountId: number, | ||
applicationId: number, | ||
platformVersion: string | ||
): HubSpotPromise<InitializeMigrationResponse> { | ||
return http.post(accountId, { | ||
url: `${MIGRATIONS_API_PATH_V2}/migrations`, | ||
data: { | ||
applicationId, | ||
platformVersion: mapPlatformVersionToEnum(platformVersion), | ||
}, | ||
}); | ||
} | ||
|
||
export async function continueMigration( | ||
portalId: number, | ||
migrationId: number, | ||
componentUids: Record<string, string>, | ||
projectName: string | ||
): HubSpotPromise<ContinueMigrationResponse> { | ||
return http.post(portalId, { | ||
url: `${MIGRATIONS_API_PATH_V2}/migrations/continue`, | ||
data: { | ||
migrationId, | ||
projectName, | ||
componentUids, | ||
}, | ||
}); | ||
} | ||
|
||
export function checkMigrationStatusV2( | ||
accountId: number, | ||
id: number | ||
): HubSpotPromise<MigrationStatus> { | ||
return http.get<MigrationStatus>(accountId, { | ||
url: `${MIGRATIONS_API_PATH_V2}/migrations/${id}/status`, | ||
}); | ||
} |
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,22 @@ | ||
import migrateCommand from './app/migrate'; | ||
import { addGlobalOptions } from '../lib/commonOpts'; | ||
import { Argv, CommandModule } from 'yargs'; | ||
|
||
export const command = ['app', 'apps']; | ||
|
||
// Keep the command hidden for now | ||
export const describe = undefined; | ||
|
||
export function builder(yargs: Argv) { | ||
addGlobalOptions(yargs); | ||
|
||
return yargs.command(migrateCommand).demandCommand(1, ''); | ||
} | ||
|
||
const appCommand: CommandModule = { | ||
command, | ||
describe, | ||
builder, | ||
handler: () => {}, | ||
}; | ||
export default appCommand; |
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.
Should these go in the
types/
folder?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.
I left them here because we don't really seem to have a hard a fast rule for putting them in the
types/
folder. We seem to have types living in files next to the methods that use them as return types, so I went this that approach since they don't really make sense as standalone types.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.
That being said I don't have a strong opinion and if we want to move this to types LMK and I will move them in a follow up, going to merge this as is.