Skip to content

Commit 62c6b6e

Browse files
authored
Merge pull request #65 from bchampp/feat/durable-functions
feat: add support for durable functions
2 parents d13daf9 + 0f554bb commit 62c6b6e

6 files changed

Lines changed: 268 additions & 22 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@ Below are two commonly used Build examples for Node.js and Python:
213213
| `logging-config` | CloudWatch Logs configuration | No | |
214214
| `code-signing-config-arn` | ARN of code-signing configuration | No | |
215215
| `tags` | Tags to apply to the function as JSON string | No | |
216+
| `durable-config` | Configuration settings for durable functions as JSON string | No | |
216217

217218
## Outputs
218219

__tests__/validations.test.js

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1204,6 +1204,190 @@ describe('Validations Tests', () => {
12041204
expect(core.setFailed).not.toHaveBeenCalled();
12051205
});
12061206
});
1207+
1208+
describe('durable-config validation', () => {
1209+
test('should accept valid durable-config with ExecutionTimeout', () => {
1210+
const mockGetInput = jest.fn((name) => {
1211+
if (name === 'durable-config') {
1212+
return '{"ExecutionTimeout":3600}';
1213+
}
1214+
const inputs = {
1215+
'function-name': 'test-function',
1216+
'region': 'us-east-1',
1217+
'code-artifacts-dir': './test-dir'
1218+
};
1219+
return inputs[name] || '';
1220+
});
1221+
core.getInput = mockGetInput;
1222+
const result = originalValidations.validateAllInputs();
1223+
expect(result.valid).toBe(true);
1224+
expect(result.parsedDurableConfig).toEqual({ ExecutionTimeout: 3600 });
1225+
expect(core.setFailed).not.toHaveBeenCalled();
1226+
});
1227+
1228+
test('should accept valid durable-config with RetentionPeriodInDays', () => {
1229+
const mockGetInput = jest.fn((name) => {
1230+
if (name === 'durable-config') {
1231+
return '{"RetentionPeriodInDays":7}';
1232+
}
1233+
const inputs = {
1234+
'function-name': 'test-function',
1235+
'region': 'us-east-1',
1236+
'code-artifacts-dir': './test-dir'
1237+
};
1238+
return inputs[name] || '';
1239+
});
1240+
core.getInput = mockGetInput;
1241+
const result = originalValidations.validateAllInputs();
1242+
expect(result.valid).toBe(true);
1243+
expect(result.parsedDurableConfig).toEqual({ RetentionPeriodInDays: 7 });
1244+
expect(core.setFailed).not.toHaveBeenCalled();
1245+
});
1246+
1247+
test('should accept valid durable-config with both properties', () => {
1248+
const mockGetInput = jest.fn((name) => {
1249+
if (name === 'durable-config') {
1250+
return '{"ExecutionTimeout":3600,"RetentionPeriodInDays":7}';
1251+
}
1252+
const inputs = {
1253+
'function-name': 'test-function',
1254+
'region': 'us-east-1',
1255+
'code-artifacts-dir': './test-dir'
1256+
};
1257+
return inputs[name] || '';
1258+
});
1259+
core.getInput = mockGetInput;
1260+
const result = originalValidations.validateAllInputs();
1261+
expect(result.valid).toBe(true);
1262+
expect(result.parsedDurableConfig).toEqual({
1263+
ExecutionTimeout: 3600,
1264+
RetentionPeriodInDays: 7
1265+
});
1266+
expect(core.setFailed).not.toHaveBeenCalled();
1267+
});
1268+
1269+
test('should reject ExecutionTimeout below minimum', () => {
1270+
const mockGetInput = jest.fn((name) => {
1271+
if (name === 'durable-config') {
1272+
return '{"ExecutionTimeout":0}';
1273+
}
1274+
const inputs = {
1275+
'function-name': 'test-function',
1276+
'region': 'us-east-1',
1277+
'code-artifacts-dir': './test-dir'
1278+
};
1279+
return inputs[name] || '';
1280+
});
1281+
core.getInput = mockGetInput;
1282+
const result = originalValidations.validateAllInputs();
1283+
expect(result.valid).toBe(false);
1284+
expect(core.setFailed).toHaveBeenCalledWith(
1285+
expect.stringContaining('ExecutionTimeout must be between 1 and 31622400')
1286+
);
1287+
});
1288+
1289+
test('should reject ExecutionTimeout above maximum', () => {
1290+
const mockGetInput = jest.fn((name) => {
1291+
if (name === 'durable-config') {
1292+
return '{"ExecutionTimeout":31622401}';
1293+
}
1294+
const inputs = {
1295+
'function-name': 'test-function',
1296+
'region': 'us-east-1',
1297+
'code-artifacts-dir': './test-dir'
1298+
};
1299+
return inputs[name] || '';
1300+
});
1301+
core.getInput = mockGetInput;
1302+
const result = originalValidations.validateAllInputs();
1303+
expect(result.valid).toBe(false);
1304+
expect(core.setFailed).toHaveBeenCalledWith(
1305+
expect.stringContaining('ExecutionTimeout must be between 1 and 31622400')
1306+
);
1307+
});
1308+
1309+
test('should reject RetentionPeriodInDays below minimum', () => {
1310+
const mockGetInput = jest.fn((name) => {
1311+
if (name === 'durable-config') {
1312+
return '{"RetentionPeriodInDays":0}';
1313+
}
1314+
const inputs = {
1315+
'function-name': 'test-function',
1316+
'region': 'us-east-1',
1317+
'code-artifacts-dir': './test-dir'
1318+
};
1319+
return inputs[name] || '';
1320+
});
1321+
core.getInput = mockGetInput;
1322+
const result = originalValidations.validateAllInputs();
1323+
expect(result.valid).toBe(false);
1324+
expect(core.setFailed).toHaveBeenCalledWith(
1325+
expect.stringContaining('RetentionPeriodInDays must be between 1 and 90')
1326+
);
1327+
});
1328+
1329+
test('should reject RetentionPeriodInDays above maximum', () => {
1330+
const mockGetInput = jest.fn((name) => {
1331+
if (name === 'durable-config') {
1332+
return '{"RetentionPeriodInDays":91}';
1333+
}
1334+
const inputs = {
1335+
'function-name': 'test-function',
1336+
'region': 'us-east-1',
1337+
'code-artifacts-dir': './test-dir'
1338+
};
1339+
return inputs[name] || '';
1340+
});
1341+
core.getInput = mockGetInput;
1342+
const result = originalValidations.validateAllInputs();
1343+
expect(result.valid).toBe(false);
1344+
expect(core.setFailed).toHaveBeenCalledWith(
1345+
expect.stringContaining('RetentionPeriodInDays must be between 1 and 90')
1346+
);
1347+
});
1348+
1349+
test('should accept maximum valid values', () => {
1350+
const mockGetInput = jest.fn((name) => {
1351+
if (name === 'durable-config') {
1352+
return '{"ExecutionTimeout":31622400,"RetentionPeriodInDays":90}';
1353+
}
1354+
const inputs = {
1355+
'function-name': 'test-function',
1356+
'region': 'us-east-1',
1357+
'code-artifacts-dir': './test-dir'
1358+
};
1359+
return inputs[name] || '';
1360+
});
1361+
core.getInput = mockGetInput;
1362+
const result = originalValidations.validateAllInputs();
1363+
expect(result.valid).toBe(true);
1364+
expect(result.parsedDurableConfig).toEqual({
1365+
ExecutionTimeout: 31622400,
1366+
RetentionPeriodInDays: 90
1367+
});
1368+
expect(core.setFailed).not.toHaveBeenCalled();
1369+
});
1370+
1371+
test('should reject invalid JSON in durable-config', () => {
1372+
const mockGetInput = jest.fn((name) => {
1373+
if (name === 'durable-config') {
1374+
return '{invalid json}';
1375+
}
1376+
const inputs = {
1377+
'function-name': 'test-function',
1378+
'region': 'us-east-1',
1379+
'code-artifacts-dir': './test-dir'
1380+
};
1381+
return inputs[name] || '';
1382+
});
1383+
core.getInput = mockGetInput;
1384+
const result = originalValidations.validateAllInputs();
1385+
expect(result.valid).toBe(false);
1386+
expect(core.setFailed).toHaveBeenCalledWith(
1387+
expect.stringContaining('Invalid JSON in durable-config')
1388+
);
1389+
});
1390+
});
12071391
});
12081392
describe('getAdditionalInputs function', () => {
12091393
test('should handle invalid publish input and default to false', () => {

action.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,9 @@ inputs:
9595
tags:
9696
description: 'Tags to apply to the function as a JSON string (e.g. {"Environment":"Production","Team":"DevOps"})'
9797
required: false
98+
durable-config:
99+
description: 'Configuration settings for durable functions as a JSON string (e.g. {"ExecutionTimeout":3600,"RetentionPeriodInDays":7})'
100+
required: false
98101
outputs:
99102
function-arn:
100103
description: 'The ARN of the updated Lambda function.'

dist/index.js

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,11 @@ async function run() {
2828
role, codeSigningConfigArn, kmsKeyArn, sourceKmsKeyArn,
2929
vpcConfig, deadLetterConfig, tracingConfig,
3030
layers, fileSystemConfigs, imageConfig, snapStart,
31-
loggingConfig, tags,
31+
loggingConfig, tags, durableConfig,
3232
parsedEnvironment, parsedVpcConfig, parsedDeadLetterConfig,
3333
parsedTracingConfig, parsedLayers, parsedFileSystemConfigs,
3434
parsedImageConfig, parsedSnapStart, parsedLoggingConfig, parsedTags,
35+
parsedDurableConfig,
3536
functionDescription, dryRun, publish, revisionId,
3637
runtime, handler, architectures
3738
} = inputs;
@@ -81,9 +82,10 @@ async function run() {
8182
revisionId, vpcConfig, parsedEnvironment, deadLetterConfig,
8283
tracingConfig, layers, fileSystemConfigs, imageConfig,
8384
snapStart, loggingConfig, tags, kmsKeyArn, codeSigningConfigArn,
85+
durableConfig,
8486
parsedVpcConfig, parsedDeadLetterConfig, parsedTracingConfig,
8587
parsedLayers, parsedFileSystemConfigs, parsedImageConfig,
86-
parsedSnapStart, parsedLoggingConfig, parsedTags
88+
parsedSnapStart, parsedLoggingConfig, parsedTags, parsedDurableConfig
8789
}, functionExists);
8890

8991
// Update function configuration
@@ -108,7 +110,8 @@ async function run() {
108110
...(fileSystemConfigs && { FileSystemConfigs: parsedFileSystemConfigs }),
109111
...(imageConfig && { ImageConfig: parsedImageConfig }),
110112
...(snapStart && { SnapStart: parsedSnapStart }),
111-
...(loggingConfig && { LoggingConfig: parsedLoggingConfig })
113+
...(loggingConfig && { LoggingConfig: parsedLoggingConfig }),
114+
...(durableConfig && { DurableConfig: parsedDurableConfig })
112115
});
113116

114117
if (configChanged) {
@@ -136,14 +139,16 @@ async function run() {
136139
imageConfig,
137140
snapStart,
138141
loggingConfig,
142+
durableConfig,
139143
parsedVpcConfig,
140144
parsedDeadLetterConfig,
141145
parsedTracingConfig,
142146
parsedLayers,
143147
parsedFileSystemConfigs,
144148
parsedImageConfig,
145149
parsedSnapStart,
146-
parsedLoggingConfig
150+
parsedLoggingConfig,
151+
parsedDurableConfig
147152
});
148153
} else {
149154
core.info('No configuration changes detected');
@@ -302,9 +307,10 @@ async function createFunction(client, inputs, functionExists) {
302307
timeout, publish, architectures, ephemeralStorage, revisionId,
303308
vpcConfig, parsedEnvironment, deadLetterConfig, tracingConfig,
304309
layers, fileSystemConfigs, imageConfig, snapStart, loggingConfig, tags,
305-
kmsKeyArn, codeSigningConfigArn, parsedVpcConfig, parsedDeadLetterConfig,
310+
kmsKeyArn, codeSigningConfigArn, durableConfig,
311+
parsedVpcConfig, parsedDeadLetterConfig,
306312
parsedTracingConfig, parsedLayers, parsedFileSystemConfigs, parsedImageConfig,
307-
parsedSnapStart, parsedLoggingConfig, parsedTags
313+
parsedSnapStart, parsedLoggingConfig, parsedTags, parsedDurableConfig
308314
} = inputs;
309315

310316
if (!functionExists) {
@@ -390,6 +396,7 @@ async function createFunction(client, inputs, functionExists) {
390396
...(tags && { Tags: parsedTags }),
391397
...(kmsKeyArn && { KMSKeyArn: kmsKeyArn }),
392398
...(codeSigningConfigArn && { CodeSigningConfigArn: codeSigningConfigArn }),
399+
...(durableConfig && { DurableConfig: parsedDurableConfig })
393400
};
394401

395402
core.info(`Creating new Lambda function: ${functionName}`);
@@ -480,10 +487,10 @@ async function updateFunctionConfiguration(client, params) {
480487
functionName, role, handler, functionDescription, parsedMemorySize,
481488
timeout, runtime, kmsKeyArn, ephemeralStorage, vpcConfig,
482489
parsedEnvironment, deadLetterConfig, tracingConfig, layers,
483-
fileSystemConfigs, imageConfig, snapStart, loggingConfig,
490+
fileSystemConfigs, imageConfig, snapStart, loggingConfig, durableConfig,
484491
parsedVpcConfig, parsedDeadLetterConfig, parsedTracingConfig,
485492
parsedLayers, parsedFileSystemConfigs, parsedImageConfig,
486-
parsedSnapStart, parsedLoggingConfig
493+
parsedSnapStart, parsedLoggingConfig, parsedDurableConfig
487494
} = params;
488495

489496
try {
@@ -505,7 +512,8 @@ async function updateFunctionConfiguration(client, params) {
505512
...(fileSystemConfigs && { FileSystemConfigs: parsedFileSystemConfigs }),
506513
...(imageConfig && { ImageConfig: parsedImageConfig }),
507514
...(snapStart && { SnapStart: parsedSnapStart }),
508-
...(loggingConfig && { LoggingConfig: parsedLoggingConfig })
515+
...(loggingConfig && { LoggingConfig: parsedLoggingConfig }),
516+
...(durableConfig && { DurableConfig: parsedDurableConfig })
509517
};
510518

511519
core.info(`Updating function configuration for ${functionName}`);
@@ -98020,10 +98028,11 @@ function validateJsonInputs() {
9802098028
const snapStart = core.getInput('snap-start', { required: false });
9802198029
const loggingConfig = core.getInput('logging-config', { required: false });
9802298030
const tags = core.getInput('tags', { required: false });
98031+
const durableConfig = core.getInput('durable-config', { required: false });
9802398032

9802498033
let parsedEnvironment, parsedVpcConfig, parsedDeadLetterConfig, parsedTracingConfig,
9802598034
parsedLayers, parsedFileSystemConfigs, parsedImageConfig, parsedSnapStart,
98026-
parsedLoggingConfig, parsedTags;
98035+
parsedLoggingConfig, parsedTags, parsedDurableConfig;
9802798036

9802898037
try {
9802998038
if (environment) {
@@ -98094,6 +98103,24 @@ function validateJsonInputs() {
9809498103
throw new Error('tags must be an object of key-value pairs');
9809598104
}
9809698105
}
98106+
98107+
if (durableConfig) {
98108+
parsedDurableConfig = parseJsonInput(durableConfig, 'durable-config');
98109+
if (parsedDurableConfig.ExecutionTimeout !== undefined) {
98110+
if (typeof parsedDurableConfig.ExecutionTimeout !== 'number' ||
98111+
parsedDurableConfig.ExecutionTimeout < 1 ||
98112+
parsedDurableConfig.ExecutionTimeout > 31622400) {
98113+
throw new Error('durable-config ExecutionTimeout must be between 1 and 31622400 seconds');
98114+
}
98115+
}
98116+
if (parsedDurableConfig.RetentionPeriodInDays !== undefined) {
98117+
if (typeof parsedDurableConfig.RetentionPeriodInDays !== 'number' ||
98118+
parsedDurableConfig.RetentionPeriodInDays < 1 ||
98119+
parsedDurableConfig.RetentionPeriodInDays > 90) {
98120+
throw new Error('durable-config RetentionPeriodInDays must be between 1 and 90 days');
98121+
}
98122+
}
98123+
}
9809798124
} catch (error) {
9809898125
core.setFailed(`Input validation error: ${error.message}`);
9809998126
return { valid: false };
@@ -98111,6 +98138,7 @@ function validateJsonInputs() {
9811198138
snapStart,
9811298139
loggingConfig,
9811398140
tags,
98141+
durableConfig,
9811498142
parsedEnvironment,
9811598143
parsedVpcConfig,
9811698144
parsedDeadLetterConfig,
@@ -98120,7 +98148,8 @@ function validateJsonInputs() {
9812098148
parsedImageConfig,
9812198149
parsedSnapStart,
9812298150
parsedLoggingConfig,
98123-
parsedTags
98151+
parsedTags,
98152+
parsedDurableConfig
9812498153
};
9812598154
}
9812698155

0 commit comments

Comments
 (0)