Skip to content

Commit 66b9469

Browse files
authored
feat: Add migration for existing project (#1424)
1 parent 1b1f47f commit 66b9469

File tree

17 files changed

+451
-256
lines changed

17 files changed

+451
-256
lines changed

api/__tests__/migrate.test.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ describe('api/migrate', () => {
1919
const mockAppId = 67890;
2020
const mockMigrationId = 54321;
2121
const mockPlatformVersion = '2025.2';
22+
const convertedPlatformVersion = 'V2025_2';
2223
const mockProjectName = 'Test Project';
2324
const mockComponentUids = { 'component-1': 'uid-1', 'component-2': 'uid-2' };
2425

@@ -51,10 +52,16 @@ describe('api/migrate', () => {
5152
// @ts-expect-error Mock
5253
httpMock.get.mockResolvedValue(mockResponse);
5354

54-
const result = await listAppsForMigration(mockAccountId);
55+
const result = await listAppsForMigration(
56+
mockAccountId,
57+
mockPlatformVersion
58+
);
5559

5660
expect(http.get).toHaveBeenCalledWith(mockAccountId, {
5761
url: 'dfs/migrations/v2/list-apps',
62+
params: {
63+
platformVersion: convertedPlatformVersion,
64+
},
5865
});
5966
expect(result).toEqual(mockResponse);
6067
});
@@ -76,7 +83,7 @@ describe('api/migrate', () => {
7683
url: 'dfs/migrations/v2/migrations',
7784
data: {
7885
applicationId: mockAppId,
79-
platformVersion: 'V2025_2',
86+
platformVersion: convertedPlatformVersion,
8087
},
8188
});
8289
expect(result).toEqual(mockResponse);
@@ -171,7 +178,7 @@ describe('api/migrate', () => {
171178
const mockResponse: MigrationStatus = {
172179
id: mockMigrationId,
173180
status: MIGRATION_STATUS.FAILURE,
174-
projectErrorsDetail: 'Error details',
181+
projectErrorDetail: 'Error details',
175182
componentErrorDetails: {
176183
'component-1': 'Component error',
177184
},

api/migrate.ts

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import {
55
} from '@hubspot/local-dev-lib/constants/projects';
66
import { http } from '@hubspot/local-dev-lib/http';
77
import { MIGRATION_STATUS } from '@hubspot/local-dev-lib/types/Migration';
8+
import { logger } from '@hubspot/local-dev-lib/logger';
9+
import util from 'util';
810

911
const MIGRATIONS_API_PATH_V2 = 'dfs/migrations/v2';
1012

@@ -72,7 +74,7 @@ export interface MigrationSuccess extends MigrationBaseStatus {
7274

7375
export interface MigrationFailed extends MigrationBaseStatus {
7476
status: typeof MIGRATION_STATUS.FAILURE;
75-
projectErrorsDetail?: string;
77+
projectErrorDetail: string;
7678
componentErrorDetails: Record<string, string>;
7779
}
7880

@@ -82,11 +84,24 @@ export type MigrationStatus =
8284
| MigrationSuccess
8385
| MigrationFailed;
8486

87+
export function isMigrationStatus(error: unknown): error is MigrationStatus {
88+
return (
89+
typeof error === 'object' &&
90+
error !== null &&
91+
'id' in error &&
92+
'status' in error
93+
);
94+
}
95+
8596
export async function listAppsForMigration(
86-
accountId: number
97+
accountId: number,
98+
platformVersion: string
8799
): HubSpotPromise<ListAppsResponse> {
88100
return http.get<ListAppsResponse>(accountId, {
89101
url: `${MIGRATIONS_API_PATH_V2}/list-apps`,
102+
params: {
103+
platformVersion: mapPlatformVersionToEnum(platformVersion),
104+
},
90105
});
91106
}
92107

@@ -128,11 +143,15 @@ export async function continueMigration(
128143
});
129144
}
130145

131-
export function checkMigrationStatusV2(
146+
export async function checkMigrationStatusV2(
132147
accountId: number,
133148
id: number
134149
): HubSpotPromise<MigrationStatus> {
135-
return http.get<MigrationStatus>(accountId, {
150+
const response = await http.get<MigrationStatus>(accountId, {
136151
url: `${MIGRATIONS_API_PATH_V2}/migrations/${id}/status`,
137152
});
153+
154+
logger.debug(util.inspect(response.data, { depth: null }));
155+
156+
return response;
138157
}

commands/__tests__/project.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import open from '../project/open';
1111
import * as dev from '../project/dev';
1212
import add from '../project/add';
1313
import migrateApp from '../project/migrateApp';
14+
import migrate from '../project/migrate';
1415
import cloneApp from '../project/cloneApp';
1516
import installDeps from '../project/installDeps';
1617

@@ -27,6 +28,7 @@ jest.mock('../project/dev');
2728
jest.mock('../project/add');
2829
jest.mock('../project/migrateApp', () => ({}));
2930
jest.mock('../project/cloneApp', () => ({}));
31+
jest.mock('../project/migrate', () => ({}));
3032
jest.mock('../project/installDeps');
3133
jest.mock('../../lib/commonOpts');
3234

@@ -68,6 +70,7 @@ describe('commands/project', () => {
6870
['migrateApp', migrateApp],
6971
['cloneApp', cloneApp],
7072
['installDeps', installDeps],
73+
['migrate', migrate],
7174
];
7275

7376
it('should demand the command takes one positional argument', () => {

commands/app/__tests__/migrate.test.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
import { ArgumentsCamelCase, Argv } from 'yargs';
22
import { handler, builder } from '../migrate';
33
import { getAccountConfig } from '@hubspot/local-dev-lib/config';
4-
import { migrateApp2025_2 } from '../../../lib/app/migrate';
4+
import { migrateApp2025_2, MigrateAppArgs } from '../../../lib/app/migrate';
55
import { migrateApp2023_2 } from '../../../lib/app/migrate_legacy';
66
import { logger } from '@hubspot/local-dev-lib/logger';
77
import { EXIT_CODES } from '../../../lib/enums/exitCodes';
8-
import { MigrateAppOptions } from '../../../types/Yargs';
98
import { PLATFORM_VERSIONS } from '@hubspot/local-dev-lib/constants/projects';
109

1110
jest.mock('@hubspot/local-dev-lib/config');
@@ -38,7 +37,7 @@ describe('commands/app/migrate', () => {
3837

3938
await handler({
4039
derivedAccountId: mockAccountId,
41-
} as ArgumentsCamelCase<MigrateAppOptions>);
40+
} as ArgumentsCamelCase<MigrateAppArgs>);
4241

4342
expect(mockedLogger.error).toHaveBeenCalled();
4443
expect(exitSpy).toHaveBeenCalledWith(EXIT_CODES.ERROR);
@@ -49,7 +48,7 @@ describe('commands/app/migrate', () => {
4948
await handler({
5049
derivedAccountId: mockAccountId,
5150
platformVersion: PLATFORM_VERSIONS.v2025_2,
52-
} as ArgumentsCamelCase<MigrateAppOptions>);
51+
} as ArgumentsCamelCase<MigrateAppArgs>);
5352

5453
expect(mockedMigrateApp2025_2).toHaveBeenCalledWith(
5554
mockAccountId,
@@ -62,7 +61,7 @@ describe('commands/app/migrate', () => {
6261
await handler({
6362
derivedAccountId: mockAccountId,
6463
platformVersion: PLATFORM_VERSIONS.v2023_2,
65-
} as ArgumentsCamelCase<MigrateAppOptions>);
64+
} as ArgumentsCamelCase<MigrateAppArgs>);
6665

6766
expect(mockedMigrateApp2023_2).toHaveBeenCalledWith(
6867
mockAccountId,
@@ -80,7 +79,7 @@ describe('commands/app/migrate', () => {
8079
await handler({
8180
derivedAccountId: mockAccountId,
8281
platformVersion: PLATFORM_VERSIONS.v2023_2,
83-
} as ArgumentsCamelCase<MigrateAppOptions>);
82+
} as ArgumentsCamelCase<MigrateAppArgs>);
8483

8584
expect(mockedLogger.error).toHaveBeenCalled();
8685
expect(exitSpy).toHaveBeenCalledWith(EXIT_CODES.ERROR);

commands/app/migrate.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,7 @@ import {
1515
import { i18n } from '../../lib/lang';
1616
import { ApiErrorContext, logError } from '../../lib/errorHandlers';
1717
import { EXIT_CODES } from '../../lib/enums/exitCodes';
18-
import { MigrateAppOptions } from '../../types/Yargs';
19-
import { migrateApp2025_2 } from '../../lib/app/migrate';
18+
import { migrateApp2025_2, MigrateAppArgs } from '../../lib/app/migrate';
2019
import { uiBetaTag, uiLink } from '../../lib/ui';
2120
import { migrateApp2023_2 } from '../../lib/app/migrate_legacy';
2221

@@ -26,7 +25,7 @@ export const validMigrationTargets = [v2023_2, v2025_2, unstable];
2625
const command = 'migrate';
2726
const describe = undefined; // uiBetaTag(i18n(`commands.project.subcommands.migrateApp.header.text.describe`), false);
2827

29-
export async function handler(options: ArgumentsCamelCase<MigrateAppOptions>) {
28+
export async function handler(options: ArgumentsCamelCase<MigrateAppArgs>) {
3029
const { derivedAccountId, platformVersion } = options;
3130
await trackCommandUsage('migrate-app', {}, derivedAccountId);
3231
const accountConfig = getAccountConfig(derivedAccountId);
@@ -86,7 +85,7 @@ export async function handler(options: ArgumentsCamelCase<MigrateAppOptions>) {
8685
return process.exit(EXIT_CODES.SUCCESS);
8786
}
8887

89-
export function builder(yargs: Argv): Argv<MigrateAppOptions> {
88+
export function builder(yargs: Argv): Argv<MigrateAppArgs> {
9089
addConfigOptions(yargs);
9190
addAccountOptions(yargs);
9291
addUseEnvironmentOptions(yargs);
@@ -125,10 +124,10 @@ export function builder(yargs: Argv): Argv<MigrateAppOptions> {
125124
],
126125
]);
127126

128-
return yargs as Argv<MigrateAppOptions>;
127+
return yargs as Argv<MigrateAppArgs>;
129128
}
130129

131-
const migrateCommand: CommandModule<unknown, MigrateAppOptions> = {
130+
const migrateCommand: CommandModule<unknown, MigrateAppArgs> = {
132131
command,
133132
describe,
134133
handler,

commands/project.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const open = require('./project/open');
1313
const dev = require('./project/dev');
1414
const add = require('./project/add');
1515
const migrateApp = require('./project/migrateApp');
16+
const migrate = require('./project/migrate');
1617
const cloneApp = require('./project/cloneApp');
1718
const installDeps = require('./project/installDeps');
1819

@@ -36,6 +37,7 @@ exports.builder = yargs => {
3637
.command(download)
3738
.command(open)
3839
.command(migrateApp)
40+
.command(migrate)
3941
.command(cloneApp)
4042
.command(installDeps)
4143
.demandCommand(1, '');

commands/project/cloneApp.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,12 @@ import { extractZipArchive } from '@hubspot/local-dev-lib/archive';
3131
import { getAccountConfig } from '@hubspot/local-dev-lib/config';
3232
import SpinniesManager from '../../lib/ui/SpinniesManager';
3333
import { ArgumentsCamelCase, Argv, CommandModule } from 'yargs';
34-
import { CloneAppArgs } from '../../types/Yargs';
34+
import {
35+
AccountArgs,
36+
CommonArgs,
37+
ConfigArgs,
38+
EnvironmentArgs,
39+
} from '../../types/Yargs';
3540
import { logInvalidAccountError } from '../../lib/app/migrate';
3641

3742
const i18nKey = 'commands.project.subcommands.cloneApp';
@@ -40,6 +45,14 @@ export const command = 'clone-app';
4045
export const describe = uiDeprecatedTag(i18n(`${i18nKey}.describe`), false);
4146
export const deprecated = true;
4247

48+
export type CloneAppArgs = ConfigArgs &
49+
EnvironmentArgs &
50+
AccountArgs &
51+
CommonArgs & {
52+
dest: string;
53+
appId: number;
54+
};
55+
4356
export const handler = async (options: ArgumentsCamelCase<CloneAppArgs>) => {
4457
const { derivedAccountId } = options;
4558
await trackCommandUsage('clone-app', {}, derivedAccountId);
@@ -54,7 +67,7 @@ export const handler = async (options: ArgumentsCamelCase<CloneAppArgs>) => {
5467
}
5568

5669
if (!isAppDeveloperAccount(accountConfig)) {
57-
logInvalidAccountError(i18nKey);
70+
logInvalidAccountError();
5871
process.exit(EXIT_CODES.SUCCESS);
5972
}
6073

commands/project/migrate.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { i18n } from '../../lib/lang';
2+
3+
import { ArgumentsCamelCase, Argv, CommandModule } from 'yargs';
4+
import { logger } from '@hubspot/local-dev-lib/logger';
5+
import {
6+
AccountArgs,
7+
CommonArgs,
8+
ConfigArgs,
9+
EnvironmentArgs,
10+
} from '../../types/Yargs';
11+
import {
12+
addAccountOptions,
13+
addConfigOptions,
14+
addGlobalOptions,
15+
} from '../../lib/commonOpts';
16+
import { migrateApp2025_2 } from '../../lib/app/migrate';
17+
import { getProjectConfig } from '../../lib/projects';
18+
import { PLATFORM_VERSIONS } from '@hubspot/local-dev-lib/constants/projects';
19+
import { logError } from '../../lib/errorHandlers';
20+
import { EXIT_CODES } from '../../lib/enums/exitCodes';
21+
22+
export type ProjectMigrateArgs = CommonArgs &
23+
AccountArgs &
24+
EnvironmentArgs &
25+
ConfigArgs & {
26+
platformVersion: string;
27+
};
28+
29+
export const command = 'migrate';
30+
31+
export const describe = undefined; // i18n('commands.project.subcommands.migrate.noProjectConfig')
32+
33+
export async function handler(
34+
options: ArgumentsCamelCase<ProjectMigrateArgs>
35+
): Promise<void> {
36+
const projectConfig = await getProjectConfig();
37+
38+
if (!projectConfig.projectConfig) {
39+
logger.error(
40+
i18n('commands.project.subcommands.migrate.errors.noProjectConfig')
41+
);
42+
return process.exit(EXIT_CODES.ERROR);
43+
}
44+
45+
const { derivedAccountId } = options;
46+
try {
47+
await migrateApp2025_2(
48+
derivedAccountId,
49+
{
50+
...options,
51+
name: projectConfig?.projectConfig?.name,
52+
platformVersion: options.platformVersion,
53+
},
54+
projectConfig
55+
);
56+
} catch (error) {
57+
logError(error);
58+
return process.exit(EXIT_CODES.ERROR);
59+
}
60+
return process.exit(EXIT_CODES.SUCCESS);
61+
}
62+
63+
export function builder(yargs: Argv): Argv<ProjectMigrateArgs> {
64+
addConfigOptions(yargs);
65+
addAccountOptions(yargs);
66+
addGlobalOptions(yargs);
67+
68+
yargs.option('platform-version', {
69+
type: 'string',
70+
choices: Object.values(PLATFORM_VERSIONS),
71+
default: PLATFORM_VERSIONS.v2025_2,
72+
hidden: true,
73+
});
74+
75+
return yargs as Argv<ProjectMigrateArgs>;
76+
}
77+
const migrateAppCommand: CommandModule<unknown, ProjectMigrateArgs> = {
78+
command,
79+
describe,
80+
handler,
81+
builder,
82+
};
83+
84+
export default migrateAppCommand;

0 commit comments

Comments
 (0)