Skip to content

Commit 71ab7d1

Browse files
committed
feat(cli-internal): allow generate to skip non-JS Lambda functions
Surface non-JS Lambda runtimes as unsupported in the assessment rather than throwing at generation time. The FunctionAssessor now reads the runtime from the local CloudFormation template and marks the resource as unsupported for generate when it is not Node.js. The generate orchestrator checks assessment.of(resource, step) before entering the switch, skipping any resource that is not supported. This means --skip-validations allows generation to proceed while silently skipping non-JS functions. Also adds Assessment.of(resource, step) which returns the support level for a specific resource, usable by any step orchestrator. Tested with unit tests covering Python, Java, dotnet runtimes, Node.js as supported, and missing template as an error. --- Prompt: Fix issue 14723 — `generate` should allow skipping over non JS functions. These functions can later be hand coded by the user. Instead of throwing an error, surface the unsupported functions as part of the assessment validation — this would allow --skip-validations to skip over this as well.
1 parent 960b782 commit 71ab7d1

5 files changed

Lines changed: 149 additions & 8 deletions

File tree

packages/amplify-cli/src/__tests__/commands/gen2-migration/assess.test.ts

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,18 @@ function mockGen1App(resources: DiscoveredResource[], existingFiles: string[] =
99
discover: () => resources,
1010
meta: () => undefined,
1111
fileExists: (path: string) => fileSet.has(path),
12-
json: (path: string) => jsonFiles[path],
12+
json: (path: string) => {
13+
if (!(path in jsonFiles)) throw new Error(`File not found: ${path}`);
14+
return jsonFiles[path];
15+
},
1316
ensureCliInputs: () => undefined,
1417
} as unknown as Gen1App;
1518
}
1619

20+
const NODEJS_TEMPLATE = {
21+
Resources: { LambdaFunction: { Properties: { Runtime: 'nodejs18.x' } } },
22+
};
23+
1724
describe('AmplifyMigrationAssessor', () => {
1825
describe('assess()', () => {
1926
it('returns empty assessment when no resources discovered', () => {
@@ -31,7 +38,10 @@ describe('AmplifyMigrationAssessor', () => {
3138
const gen1App = mockGen1App(
3239
[{ category: 'function', resourceName: 'myFunc', service: 'Lambda', key: 'function:Lambda' }],
3340
['function/myFunc/custom-policies.json'],
34-
{ 'function/myFunc/custom-policies.json': [{ Action: ['s3:GetObject'], Resource: ['arn:aws:s3:::my-bucket/*'] }] },
41+
{
42+
'function/myFunc/myFunc-cloudformation-template.json': NODEJS_TEMPLATE,
43+
'function/myFunc/custom-policies.json': [{ Action: ['s3:GetObject'], Resource: ['arn:aws:s3:::my-bucket/*'] }],
44+
},
3545
);
3646
const assessor = new AmplifyMigrationAssessor(gen1App);
3747
const assessment = assessor.assess();
@@ -56,7 +66,10 @@ describe('AmplifyMigrationAssessor', () => {
5666
const gen1App = mockGen1App(
5767
[{ category: 'function', resourceName: 'myFunc', service: 'Lambda', key: 'function:Lambda' }],
5868
['function/myFunc/custom-policies.json'],
59-
{ 'function/myFunc/custom-policies.json': [{ Action: [], Resource: [] }] },
69+
{
70+
'function/myFunc/myFunc-cloudformation-template.json': NODEJS_TEMPLATE,
71+
'function/myFunc/custom-policies.json': [{ Action: [], Resource: [] }],
72+
},
6073
);
6174
const assessor = new AmplifyMigrationAssessor(gen1App);
6275
const assessment = assessor.assess();
@@ -73,6 +86,26 @@ describe('AmplifyMigrationAssessor', () => {
7386
expect(assessment.resources[0].generate.level).toBe('unsupported');
7487
expect(assessment.resources[0].refactor.level).toBe('unsupported');
7588
});
89+
90+
it('marks function with non-JS runtime as unsupported for generate', () => {
91+
const gen1App = mockGen1App(
92+
[{ category: 'function', resourceName: 'myPythonFunc', service: 'Lambda', key: 'function:Lambda' }],
93+
['function/myPythonFunc/myPythonFunc-cloudformation-template.json'],
94+
{
95+
'function/myPythonFunc/myPythonFunc-cloudformation-template.json': {
96+
Resources: { LambdaFunction: { Properties: { Runtime: 'python3.11' } } },
97+
},
98+
},
99+
);
100+
const assessor = new AmplifyMigrationAssessor(gen1App);
101+
const assessment = assessor.assess();
102+
103+
expect(assessment.resources).toHaveLength(1);
104+
expect(assessment.resources[0].generate.level).toBe('unsupported');
105+
expect(assessment.resources[0].refactor.level).toBe('supported');
106+
expect(assessment.validFor('generate')).toBe(false);
107+
expect(assessment.validFor('refactor')).toBe(true);
108+
});
76109
});
77110

78111
describe('run()', () => {

packages/amplify-cli/src/__tests__/commands/gen2-migration/assess/function/function.assessor.test.ts

Lines changed: 76 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,25 @@ function mockGen1App(existingFiles: string[] = [], jsonFiles: Record<string, unk
66
const fileSet = new Set(existingFiles);
77
return {
88
fileExists: (path: string) => fileSet.has(path),
9-
json: (path: string) => jsonFiles[path],
9+
json: (path: string) => {
10+
if (!(path in jsonFiles)) throw new Error(`File not found: ${path}`);
11+
return jsonFiles[path];
12+
},
1013
} as unknown as Gen1App;
1114
}
1215

16+
const NODEJS_TEMPLATE_PATH = 'function/myFunc/myFunc-cloudformation-template.json';
17+
const NODEJS_TEMPLATE = {
18+
Resources: { LambdaFunction: { Properties: { Runtime: 'nodejs18.x' } } },
19+
};
20+
1321
const RESOURCE: DiscoveredResource = { category: 'function', resourceName: 'myFunc', service: 'Lambda', key: 'function:Lambda' };
1422

1523
describe('FunctionAssessor', () => {
1624
it('records resource as supported', () => {
25+
const gen1App = mockGen1App([], { [NODEJS_TEMPLATE_PATH]: NODEJS_TEMPLATE });
1726
const assessment = new Assessment('app', 'dev');
18-
new FunctionAssessor(mockGen1App(), RESOURCE).record(assessment);
27+
new FunctionAssessor(gen1App, RESOURCE).record(assessment);
1928

2029
const entry = assessment.resources[0];
2130
expect(entry!.generate.level).toBe('supported');
@@ -24,6 +33,7 @@ describe('FunctionAssessor', () => {
2433

2534
it('detects non-empty custom-policies.json', () => {
2635
const gen1App = mockGen1App(['function/myFunc/custom-policies.json'], {
36+
[NODEJS_TEMPLATE_PATH]: NODEJS_TEMPLATE,
2737
'function/myFunc/custom-policies.json': [{ Action: ['s3:GetObject'], Resource: ['arn:aws:s3:::bucket/*'] }],
2838
});
2939
const assessment = new Assessment('app', 'dev');
@@ -39,6 +49,7 @@ describe('FunctionAssessor', () => {
3949

4050
it('ignores empty custom-policies.json', () => {
4151
const gen1App = mockGen1App(['function/myFunc/custom-policies.json'], {
52+
[NODEJS_TEMPLATE_PATH]: NODEJS_TEMPLATE,
4253
'function/myFunc/custom-policies.json': [{ Action: [], Resource: [] }],
4354
});
4455
const assessment = new Assessment('app', 'dev');
@@ -48,9 +59,71 @@ describe('FunctionAssessor', () => {
4859
});
4960

5061
it('records no features when custom-policies.json is absent', () => {
62+
const gen1App = mockGen1App([], { [NODEJS_TEMPLATE_PATH]: NODEJS_TEMPLATE });
5163
const assessment = new Assessment('app', 'dev');
52-
new FunctionAssessor(mockGen1App(), RESOURCE).record(assessment);
64+
new FunctionAssessor(gen1App, RESOURCE).record(assessment);
5365

5466
expect(assessment.features).toHaveLength(0);
5567
});
68+
69+
describe('non-JS runtime detection', () => {
70+
it('marks resource as unsupported for generate when runtime is Python', () => {
71+
const gen1App = mockGen1App(['function/myFunc/myFunc-cloudformation-template.json'], {
72+
'function/myFunc/myFunc-cloudformation-template.json': {
73+
Resources: { LambdaFunction: { Properties: { Runtime: 'python3.11' } } },
74+
},
75+
});
76+
const assessment = new Assessment('app', 'dev');
77+
new FunctionAssessor(gen1App, RESOURCE).record(assessment);
78+
79+
const entry = assessment.resources[0];
80+
expect(entry!.generate.level).toBe('unsupported');
81+
expect(entry!.generate.note).toContain('python3.11');
82+
expect(entry!.refactor.level).toBe('supported');
83+
});
84+
85+
it('records non-js-runtime as unsupported resource without feature entry', () => {
86+
const gen1App = mockGen1App(['function/myFunc/myFunc-cloudformation-template.json'], {
87+
'function/myFunc/myFunc-cloudformation-template.json': {
88+
Resources: { LambdaFunction: { Properties: { Runtime: 'java21' } } },
89+
},
90+
});
91+
const assessment = new Assessment('app', 'dev');
92+
new FunctionAssessor(gen1App, RESOURCE).record(assessment);
93+
94+
expect(assessment.resources[0]!.generate.level).toBe('unsupported');
95+
expect(assessment.features).toHaveLength(0);
96+
});
97+
98+
it('fails assessment validFor generate when runtime is non-JS', () => {
99+
const gen1App = mockGen1App(['function/myFunc/myFunc-cloudformation-template.json'], {
100+
'function/myFunc/myFunc-cloudformation-template.json': {
101+
Resources: { LambdaFunction: { Properties: { Runtime: 'dotnet8' } } },
102+
},
103+
});
104+
const assessment = new Assessment('app', 'dev');
105+
new FunctionAssessor(gen1App, RESOURCE).record(assessment);
106+
107+
expect(assessment.validFor('generate')).toBe(false);
108+
expect(assessment.validFor('refactor')).toBe(true);
109+
});
110+
111+
it('treats nodejs runtimes as supported', () => {
112+
const gen1App = mockGen1App(['function/myFunc/myFunc-cloudformation-template.json'], {
113+
'function/myFunc/myFunc-cloudformation-template.json': {
114+
Resources: { LambdaFunction: { Properties: { Runtime: 'nodejs18.x' } } },
115+
},
116+
});
117+
const assessment = new Assessment('app', 'dev');
118+
new FunctionAssessor(gen1App, RESOURCE).record(assessment);
119+
120+
expect(assessment.resources[0]!.generate.level).toBe('supported');
121+
expect(assessment.features).toHaveLength(0);
122+
});
123+
124+
it('throws when CloudFormation template is missing', () => {
125+
const assessment = new Assessment('app', 'dev');
126+
expect(() => new FunctionAssessor(mockGen1App(), RESOURCE).record(assessment)).toThrow();
127+
});
128+
});
56129
});

packages/amplify-cli/src/commands/gen2-migration/assess/assessment.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,25 @@ export class Assessment {
9898
return this._features;
9999
}
100100

101+
/**
102+
* Returns the support level for a specific resource in the given step.
103+
*/
104+
// eslint-disable-next-line consistent-return -- exhaustive switch; compiler enforces all cases
105+
public of(resource: DiscoveredResource, step: 'generate' | 'refactor'): Support {
106+
const entry = this._resources.find(
107+
(ra) => ra.resource.category === resource.category && ra.resource.resourceName === resource.resourceName,
108+
);
109+
if (!entry) {
110+
return supported();
111+
}
112+
switch (step) {
113+
case 'generate':
114+
return entry.generate;
115+
case 'refactor':
116+
return entry.refactor;
117+
}
118+
}
119+
101120
/**
102121
* Returns true if all resources and features are supported for the given step.
103122
*/

packages/amplify-cli/src/commands/gen2-migration/assess/function/function.assessor.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { Gen1App, DiscoveredResource, KNOWN_FEATURES } from '../../generate/_inf
44

55
/**
66
* Assesses migration readiness for a single Lambda function resource.
7-
* Detects custom-policies.json usage.
7+
* Detects non-JS runtimes and custom-policies.json usage.
88
*/
99
export class FunctionAssessor implements Assessor {
1010
public constructor(private readonly gen1App: Gen1App, private readonly resource: DiscoveredResource) {}
@@ -13,9 +13,13 @@ export class FunctionAssessor implements Assessor {
1313
* Records resource-level and feature-level support for this function.
1414
*/
1515
public record(assessment: Assessment): void {
16+
const templatePath = `function/${this.resource.resourceName}/${this.resource.resourceName}-cloudformation-template.json`;
17+
const template = this.gen1App.json(templatePath);
18+
const runtime: string | undefined = template?.Resources?.LambdaFunction?.Properties?.Runtime;
19+
1620
assessment.recordResource({
1721
resource: this.resource,
18-
generate: supported(),
22+
generate: this.isNonJsRuntime(runtime) ? unsupported(`uses non-JS runtime '${runtime}'`) : supported(),
1923
refactor: supported(),
2024
});
2125

@@ -30,6 +34,13 @@ export class FunctionAssessor implements Assessor {
3034
}
3135
}
3236

37+
/**
38+
* Returns true if the runtime is present and is not a Node.js variant.
39+
*/
40+
private isNonJsRuntime(runtime: string | undefined): boolean {
41+
return runtime !== undefined && !runtime.startsWith('nodejs');
42+
}
43+
3344
/**
3445
* Returns true if the function has non-empty custom policies.
3546
* The file always exists but defaults to `[{"Action":[],"Resource":[]}]`.

packages/amplify-cli/src/commands/gen2-migration/generate.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,11 @@ export class AmplifyMigrationGenerateStep extends AmplifyMigrationStep {
7171
const discovered = this.gen1App.discover();
7272

7373
for (const resource of discovered) {
74+
// Skip resources the assessment marked as unsupported for this step.
75+
if (assessment.of(resource, 'generate').level !== 'supported') {
76+
continue;
77+
}
78+
7479
switch (resource.key) {
7580
case 'auth:Cognito': {
7681
const isReferenceAuth = discovered

0 commit comments

Comments
 (0)