diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 24a07042..ca728d85 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -1,8 +1,8 @@ -on: - [pull_request] - name: Check +on: + pull_request: + jobs: check: name: Run Unit Tests diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 00000000..fe3b14b1 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,16 @@ +name: Lint + +on: + pull_request: + +jobs: + lint: + name: Run Linter + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v5 + - name: Run eslint + run: | + npm ci + npm run lint diff --git a/__tests__/deep_equal.test.js b/__tests__/deep_equal.test.js index 4311e259..ba1d38f5 100644 --- a/__tests__/deep_equal.test.js +++ b/__tests__/deep_equal.test.js @@ -107,7 +107,7 @@ describe('Deep Equal Tests', () => { }); test('Handle lambda function configuration objects', () => { - const lambdaConfig1 = { + const lambdaConfig1 = { FunctionName: 'test-function', Runtime: 'nodejs18.x', MemorySize: 512, diff --git a/__tests__/function_create.test.js b/__tests__/function_create.test.js index aa1e6421..cf0f6860 100644 --- a/__tests__/function_create.test.js +++ b/__tests__/function_create.test.js @@ -1,7 +1,6 @@ const fs = require('fs/promises'); -const path = require('path'); const core = require('@actions/core'); -const { LambdaClient, GetFunctionConfigurationCommand, CreateFunctionCommand, UpdateFunctionCodeCommand, GetFunctionCommand} = require('@aws-sdk/client-lambda'); +const { LambdaClient, GetFunctionConfigurationCommand, CreateFunctionCommand } = require('@aws-sdk/client-lambda'); const index = require('../index'); const { checkFunctionExists } = index; diff --git a/__tests__/has_configuration_changed.test.js b/__tests__/has_configuration_changed.test.js index 7e06acb2..e44892e8 100644 --- a/__tests__/has_configuration_changed.test.js +++ b/__tests__/has_configuration_changed.test.js @@ -1,5 +1,5 @@ const core = require('@actions/core'); -const { isEmptyValue, cleanNullKeys, hasConfigurationChanged, deepEqual } = require('../index'); +const { isEmptyValue, cleanNullKeys, hasConfigurationChanged } = require('../index'); jest.mock('@actions/core'); diff --git a/__tests__/package_artifacts.test.js b/__tests__/package_artifacts.test.js index 64145dc3..95b9cd02 100644 --- a/__tests__/package_artifacts.test.js +++ b/__tests__/package_artifacts.test.js @@ -176,7 +176,6 @@ describe('Package Artifacts Tests', () => { test('should handle error during directory cleanup', async () => { - const expectedTempDir = '/mock/tmp/lambda-temp-1234567890'; const expectedZipPath = '/mock/tmp/lambda-function-1234567890.zip'; const rmError = new Error('Failed to remove directory'); @@ -212,7 +211,7 @@ describe('Package Artifacts Tests', () => { test('should handle error during file copying', async () => { - fs.cp.mockImplementation((src, dest, options) => { + fs.cp.mockImplementation((src, _dest, _options) => { if (src.includes('file1.js')) { return Promise.reject(new Error('Failed to copy file')); } diff --git a/__tests__/s3_bucket_operations.test.js b/__tests__/s3_bucket_operations.test.js index 3d8868e7..e18c19cb 100644 --- a/__tests__/s3_bucket_operations.test.js +++ b/__tests__/s3_bucket_operations.test.js @@ -242,17 +242,13 @@ describe('S3 Bucket Operations Tests', () => { jest.spyOn(mainModule, 'createBucket').mockResolvedValue(true); mockS3Send.mockResolvedValueOnce({}); - try { - await mainModule.uploadToS3( - '/path/to/deployment.zip', - 'new-bucket', - 'lambda/function.zip', - 'us-east-1', - '123456789012' - ); - } catch (error) { - throw error; - } + await mainModule.uploadToS3( + '/path/to/deployment.zip', + 'new-bucket', + 'lambda/function.zip', + 'us-east-1', + '123456789012' + ); expect(core.info).toHaveBeenCalledWith(expect.stringContaining('Bucket new-bucket does not exist. Attempting to create it')); expect(mockS3Send).toHaveBeenCalledWith(expect.any(PutObjectCommand)); }); @@ -649,7 +645,6 @@ describe('S3 Bucket Operations Tests', () => { mainModule.uploadToS3.mockReset(); - const functionName = core.getInput('function-name'); const s3Bucket = core.getInput('s3-bucket'); if (s3Bucket) { await mainModule.uploadToS3('file.zip', s3Bucket, 'key.zip', 'region'); diff --git a/__tests__/update_function_code.test.js b/__tests__/update_function_code.test.js index 88b9d65b..8d10115f 100644 --- a/__tests__/update_function_code.test.js +++ b/__tests__/update_function_code.test.js @@ -85,11 +85,6 @@ describe('Lambda Function Code Tests', () => { const originalUpdateFunctionCode = index.updateFunctionCode; index.updateFunctionCode = jest.fn().mockImplementation(async (client, params) => { - const s3Result = { - bucket: params.s3Bucket, - key: params.s3Key - }; - const command = new UpdateFunctionCodeCommand({ FunctionName: params.functionName, S3Bucket: params.s3Bucket, diff --git a/__tests__/validations.test.js b/__tests__/validations.test.js index d92ad9e1..877985ea 100644 --- a/__tests__/validations.test.js +++ b/__tests__/validations.test.js @@ -40,12 +40,12 @@ describe('Validations Tests', () => { test('should handle empty memory size input', () => { jest.clearAllMocks(); core.getInput.mockImplementation((name) => { - if (name === 'memory-size') return ''; - if (name === 'function-name') return 'test-function'; - if (name === 'region') return 'us-east-1'; - if (name === 'code-artifacts-dir') return './artifacts'; - if (name === 'handler') return 'index.handler'; - if (name === 'runtime') return 'nodejs18.x'; + if (name === 'memory-size') return ''; + if (name === 'function-name') return 'test-function'; + if (name === 'region') return 'us-east-1'; + if (name === 'code-artifacts-dir') return './artifacts'; + if (name === 'handler') return 'index.handler'; + if (name === 'runtime') return 'nodejs18.x'; return ''; }); const result = originalValidations.validateAllInputs(); @@ -56,12 +56,12 @@ describe('Validations Tests', () => { test('should handle non-numeric memory size input', () => { jest.clearAllMocks(); core.getInput.mockImplementation((name) => { - if (name === 'memory-size') return 'hello'; - if (name === 'function-name') return 'test-function'; - if (name === 'region') return 'us-east-1'; - if (name === 'code-artifacts-dir') return './artifacts'; - if (name === 'handler') return 'index.handler'; - if (name === 'runtime') return 'nodejs18.x'; + if (name === 'memory-size') return 'hello'; + if (name === 'function-name') return 'test-function'; + if (name === 'region') return 'us-east-1'; + if (name === 'code-artifacts-dir') return './artifacts'; + if (name === 'handler') return 'index.handler'; + if (name === 'runtime') return 'nodejs18.x'; return ''; }); const result = originalValidations.validateAllInputs(); @@ -93,13 +93,13 @@ describe('Validations Tests', () => { test('should handle non-numeric memory size input', () => { jest.clearAllMocks(); core.getInput.mockImplementation((name) => { - if (name === 'timeout') return 'hello'; - if (name === 'function-name') return 'test-function'; - if (name === 'region') return 'us-east-1'; - if (name === 'code-artifacts-dir') return './artifacts'; - if (name === 'handler') return 'index.handler'; - if (name === 'runtime') return 'nodejs18.x'; - return ''; + if (name === 'timeout') return 'hello'; + if (name === 'function-name') return 'test-function'; + if (name === 'region') return 'us-east-1'; + if (name === 'code-artifacts-dir') return './artifacts'; + if (name === 'handler') return 'index.handler'; + if (name === 'runtime') return 'nodejs18.x'; + return ''; }); const result = originalValidations.validateAllInputs(); expect(result.valid).toBe(false); @@ -130,13 +130,13 @@ describe('Validations Tests', () => { test('should handle non-numeric memory size input', () => { jest.clearAllMocks(); core.getInput.mockImplementation((name) => { - if (name === 'ephemeral-storage') return 'hello'; - if (name === 'function-name') return 'test-function'; - if (name === 'region') return 'us-east-1'; - if (name === 'code-artifacts-dir') return './artifacts'; - if (name === 'handler') return 'index.handler'; - if (name === 'runtime') return 'nodejs18.x'; - return ''; + if (name === 'ephemeral-storage') return 'hello'; + if (name === 'function-name') return 'test-function'; + if (name === 'region') return 'us-east-1'; + if (name === 'code-artifacts-dir') return './artifacts'; + if (name === 'handler') return 'index.handler'; + if (name === 'runtime') return 'nodejs18.x'; + return ''; }); const result = originalValidations.validateAllInputs(); expect(result.valid).toBe(false); @@ -353,7 +353,7 @@ describe('Validations Tests', () => { test('should reject invalid source KMS key ARN format', () => { core.getInput.mockImplementation((name) => { if (name === 'kms-key-arn') return 'invalid:kms:key:arn'; - if (name === 'source-kms-key-arn') return 'invalid:kms:key:arn' + if (name === 'source-kms-key-arn') return 'invalid:kms:key:arn'; if (name === 'function-name') return 'test-function'; if (name === 'region') return 'us-east-1'; if (name === 'code-artifacts-dir') return './artifacts'; @@ -402,7 +402,7 @@ describe('Validations Tests', () => { test('should accept valid environment variables', () => { const mockGetInput = jest.fn((name) => { if (name === 'environment') { - return '{"ENV":"prod","DEBUG":"true","API_URL":"https://api.example.com"}' + return '{"ENV":"prod","DEBUG":"true","API_URL":"https://api.example.com"}'; } const inputs = { 'function-name': 'test-function', @@ -889,7 +889,7 @@ describe('Validations Tests', () => { describe('VPC Configuration Edge Cases', () => { test('should reject vpc-config with malformed SubnetIds', () => { const invalidVpcConfig = JSON.stringify({ - SubnetIds: "subnet-123", + SubnetIds: 'subnet-123', SecurityGroupIds: ['sg-123'] }); core.getInput.mockImplementation((name) => { @@ -906,7 +906,7 @@ describe('Validations Tests', () => { const result = originalValidations.validateAllInputs(); expect(result.valid).toBe(false); expect(core.setFailed).toHaveBeenCalledWith( - expect.stringContaining("vpc-config must include 'SubnetIds' as an array") + expect.stringContaining('vpc-config must include \'SubnetIds\' as an array') ); }); test('should reject vpc-config with empty SecurityGroupIds array', () => { @@ -1010,7 +1010,7 @@ describe('Validations Tests', () => { const result = originalValidations.validateAllInputs(); expect(result.valid).toBe(false); expect(core.setFailed).toHaveBeenCalledWith( - expect.stringContaining("tracing-config Mode must be 'Active' or 'PassThrough'") + expect.stringContaining('tracing-config Mode must be \'Active\' or \'PassThrough\'') ); }); }); @@ -1057,7 +1057,7 @@ describe('Validations Tests', () => { const result = originalValidations.validateAllInputs(); expect(result.valid).toBe(false); expect(core.setFailed).toHaveBeenCalledWith( - expect.stringContaining("snap-start ApplyOn must be 'PublishedVersions' or 'None'") + expect.stringContaining('snap-start ApplyOn must be \'PublishedVersions\' or \'None\'') ); }); }); @@ -1077,7 +1077,7 @@ describe('Validations Tests', () => { const result = originalValidations.validateAllInputs(); expect(result.valid).toBe(false); expect(core.setFailed).toHaveBeenCalledWith( - expect.stringContaining("file-system-configs must be an array") + expect.stringContaining('file-system-configs must be an array') ); }); test('should reject file-system-configs with missing Arn', () => { @@ -1096,7 +1096,7 @@ describe('Validations Tests', () => { const result = originalValidations.validateAllInputs(); expect(result.valid).toBe(false); expect(core.setFailed).toHaveBeenCalledWith( - expect.stringContaining("Each file-system-config must include 'Arn' and 'LocalMountPath'") + expect.stringContaining('Each file-system-config must include \'Arn\' and \'LocalMountPath\'') ); }); test('should validate multiple file system configs', () => { @@ -1154,7 +1154,7 @@ describe('Validations Tests', () => { const result = originalValidations.validateAllInputs(); expect(result.valid).toBe(false); expect(core.setFailed).toHaveBeenCalledWith( - expect.stringContaining("tags must be an object of key-value pairs") + expect.stringContaining('tags must be an object of key-value pairs') ); }); }); diff --git a/dist/index.js b/dist/index.js index c2c10a44..d4d5bca3 100644 --- a/dist/index.js +++ b/dist/index.js @@ -23,10 +23,10 @@ async function run() { } const { - functionName, packageType, codeArtifactsDir, imageUri, + functionName, codeArtifactsDir, ephemeralStorage, parsedMemorySize, timeout, role, codeSigningConfigArn, kmsKeyArn, sourceKmsKeyArn, - environment, vpcConfig, deadLetterConfig, tracingConfig, + vpcConfig, deadLetterConfig, tracingConfig, layers, fileSystemConfigs, imageConfig, snapStart, loggingConfig, tags, parsedEnvironment, parsedVpcConfig, parsedDeadLetterConfig, @@ -68,18 +68,13 @@ async function run() { } } - // Creating zip file (only for Zip package type) - let finalZipPath = null; - if (packageType === 'Zip') { - core.info(`Packaging code artifacts from ${codeArtifactsDir}`); - finalZipPath = await packageCodeArtifacts(codeArtifactsDir); - } else if (packageType === 'Image') { - core.info(`Using container image: ${imageUri}`); - } + // Creating zip file + core.info(`Packaging code artifacts from ${codeArtifactsDir}`); + let finalZipPath = await packageCodeArtifacts(codeArtifactsDir); // Create function await createFunction(client, { - functionName, packageType, region, finalZipPath, imageUri, dryRun, role, + functionName, region, finalZipPath, dryRun, role, s3Bucket, s3Key, sourceKmsKeyArn, runtime, handler, functionDescription, parsedMemorySize, timeout, publish, architectures, ephemeralStorage, @@ -96,30 +91,22 @@ async function run() { const configCommand = new GetFunctionConfigurationCommand({FunctionName: functionName}); let currentConfig = await client.send(configCommand); - // Check if package type is being changed (not supported by AWS) - if (currentConfig.PackageType && currentConfig.PackageType !== packageType) { - core.setFailed(`Cannot change package type of existing Lambda function from ${currentConfig.PackageType} to ${packageType}`); - return; - } - const configChanged = hasConfigurationChanged(currentConfig, { ...(role && { Role: role }), - // Only include handler, runtime, and layers for Zip package type - ...(packageType === 'Zip' && handler && { Handler: handler }), + ...(handler && { Handler: handler }), ...(functionDescription && { Description: functionDescription }), ...(parsedMemorySize && { MemorySize: parsedMemorySize }), ...(timeout && { Timeout: timeout }), - ...(packageType === 'Zip' && runtime && { Runtime: runtime }), + ...(runtime && { Runtime: runtime }), ...(kmsKeyArn && { KMSKeyArn: kmsKeyArn }), ...(ephemeralStorage && { EphemeralStorage: { Size: ephemeralStorage } }), ...(vpcConfig && { VpcConfig: parsedVpcConfig }), Environment: { Variables: parsedEnvironment }, ...(deadLetterConfig && { DeadLetterConfig: parsedDeadLetterConfig }), ...(tracingConfig && { TracingConfig: parsedTracingConfig }), - ...(packageType === 'Zip' && layers && { Layers: parsedLayers }), + ...(layers && { Layers: parsedLayers }), ...(fileSystemConfigs && { FileSystemConfigs: parsedFileSystemConfigs }), - // Only include ImageConfig for Image package type - ...(packageType === 'Image' && imageConfig && { ImageConfig: parsedImageConfig }), + ...(imageConfig && { ImageConfig: parsedImageConfig }), ...(snapStart && { SnapStart: parsedSnapStart }), ...(loggingConfig && { LoggingConfig: parsedLoggingConfig }) }); @@ -133,29 +120,28 @@ async function run() { await updateFunctionConfiguration(client, { functionName, role, - // Only include handler, runtime, and layers for Zip package type - ...(packageType === 'Zip' && { handler }), + handler, functionDescription, parsedMemorySize, timeout, - ...(packageType === 'Zip' && { runtime }), + runtime, kmsKeyArn, ephemeralStorage, vpcConfig, parsedEnvironment, deadLetterConfig, tracingConfig, - ...(packageType === 'Zip' && { layers }), + layers, fileSystemConfigs, - ...(packageType === 'Image' && { imageConfig }), + imageConfig, snapStart, loggingConfig, parsedVpcConfig, parsedDeadLetterConfig, parsedTracingConfig, - ...(packageType === 'Zip' && { parsedLayers }), + parsedLayers, parsedFileSystemConfigs, - ...(packageType === 'Image' && { parsedImageConfig }), + parsedImageConfig, parsedSnapStart, parsedLoggingConfig }); @@ -166,8 +152,6 @@ async function run() { // Update Function Code await updateFunctionCode(client, { functionName, - packageType, - imageUri, finalZipPath, useS3Method, s3Bucket, @@ -205,11 +189,12 @@ async function packageCodeArtifacts(artifactsDir) { const zipPath = path.join((__nccwpck_require__(2037).tmpdir)(), `lambda-function-${Date.now()}.zip`); try { - try { - await fs.rm(tempDir, { recursive: true, force: true }); - } catch (error) { - } + await fs.rm(tempDir, { recursive: true, force: true }); + } catch (e) { + core.info(`Failed to remove directory ${tempDir}, continuing: ${e}`); + } + try { await fs.mkdir(tempDir, { recursive: true }); const workingDir = process.cwd(); @@ -312,7 +297,7 @@ async function checkFunctionExists(client, functionName) { // Helper functions for creating Lambda function async function createFunction(client, inputs, functionExists) { const { - functionName, packageType, region, finalZipPath, imageUri, dryRun, role, s3Bucket, s3Key, + functionName, region, finalZipPath, dryRun, role, s3Bucket, s3Key, sourceKmsKeyArn, runtime, handler, functionDescription, parsedMemorySize, timeout, publish, architectures, ephemeralStorage, revisionId, vpcConfig, parsedEnvironment, deadLetterConfig, tracingConfig, @@ -323,133 +308,120 @@ async function createFunction(client, inputs, functionExists) { } = inputs; if (!functionExists) { - if (dryRun) { - core.setFailed('DRY RUN MODE can only be used for updating function code of existing functions'); - return; - } + if (dryRun) { + core.setFailed('DRY RUN MODE can only be used for updating function code of existing functions'); + return; + } - core.info(`Function ${functionName} doesn't exist, creating new function`); + core.info(`Function ${functionName} doesn't exist, creating new function`); - if(!role) { - core.setFailed('Role ARN must be provided when creating a new function'); - return; - } + if(!role) { + core.setFailed('Role ARN must be provided when creating a new function'); + return; + } - try { - core.info(`Creating Lambda function with ${packageType} package type`); + try { + core.info('Creating Lambda function with deployment package'); + + let codeParameter; - let codeParameter; + if (s3Bucket) { + try { + await uploadToS3(finalZipPath, s3Bucket, s3Key, region); + core.info(`Successfully uploaded package to S3: s3://${s3Bucket}/${s3Key}`); - if (packageType === 'Image') { - // For container images, use ImageUri - core.info(`Using container image: ${imageUri}`); codeParameter = { - ImageUri: imageUri + S3Bucket: s3Bucket, + S3Key: s3Key, + ...(sourceKmsKeyArn && { SourceKmsKeyArn: sourceKmsKeyArn }) }; - } else { - // For Zip packages, handle S3 or direct upload - if (s3Bucket) { - try { - await uploadToS3(finalZipPath, s3Bucket, s3Key, region); - core.info(`Successfully uploaded package to S3: s3://${s3Bucket}/${s3Key}`); - - codeParameter = { - S3Bucket: s3Bucket, - S3Key: s3Key, - ...(sourceKmsKeyArn && { SourceKmsKeyArn: sourceKmsKeyArn }) - }; - } catch (error) { - core.setFailed(`Failed to upload package to S3: ${error.message}`); - if (error.stack) { - core.debug(error.stack); - } - throw error; - } - } else { - try { - const zipFileContent = await fs.readFile(finalZipPath); - core.info(`Zip file read successfully, size: ${zipFileContent.length} bytes`); + } catch (error) { + core.setFailed(`Failed to upload package to S3: ${error.message}`); + if (error.stack) { + core.debug(error.stack); + } + throw error; + } + } else { + try { + const zipFileContent = await fs.readFile(finalZipPath); + core.info(`Zip file read successfully, size: ${zipFileContent.length} bytes`); - codeParameter = { - ZipFile: zipFileContent, - ...(sourceKmsKeyArn && { SourceKmsKeyArn: sourceKmsKeyArn }) - }; - } catch (error) { - if (error.code === 'EACCES') { - core.setFailed(`Failed to read Lambda deployment package: Permission denied`); - core.error('Permission denied. Check file access permissions.'); - } else { - core.setFailed(`Failed to read Lambda deployment package: ${error.message}`); - } - if (error.stack) { - core.debug(error.stack); - } - throw error; - } + codeParameter = { + ZipFile: zipFileContent, + ...(sourceKmsKeyArn && { SourceKmsKeyArn: sourceKmsKeyArn }) + }; + } catch (error) { + if (error.code === 'EACCES') { + core.setFailed(`Failed to read Lambda deployment package: Permission denied`); + core.error('Permission denied. Check file access permissions.'); + } else { + core.setFailed(`Failed to read Lambda deployment package: ${error.message}`); + } + if (error.stack) { + core.debug(error.stack); } + throw error; } + } - const input = { - FunctionName: functionName, - Code: codeParameter, - PackageType: packageType, - ...(role && { Role: role }), - // Only include runtime and handler for Zip packages - ...(packageType === 'Zip' && runtime && { Runtime: runtime }), - ...(packageType === 'Zip' && handler && { Handler: handler }), - ...(functionDescription && { Description: functionDescription }), - ...(parsedMemorySize && { MemorySize: parsedMemorySize }), - ...(timeout && { Timeout: timeout }), - ...(publish !== undefined && { Publish: publish }), - ...(architectures && { Architectures: Array.isArray(architectures) ? architectures : [architectures] }), - ...(ephemeralStorage && { EphemeralStorage: { Size: ephemeralStorage } }), - ...(revisionId && { RevisionId: revisionId }), - ...(vpcConfig && { VpcConfig: parsedVpcConfig }), - Environment: { Variables: parsedEnvironment }, - ...(deadLetterConfig && { DeadLetterConfig: parsedDeadLetterConfig }), - ...(tracingConfig && { TracingConfig: parsedTracingConfig }), - // Only include Layers for Zip package type - ...(packageType === 'Zip' && layers && { Layers: parsedLayers }), - ...(fileSystemConfigs && { FileSystemConfigs: parsedFileSystemConfigs }), - // Only include ImageConfig for Image package type - ...(packageType === 'Image' && imageConfig && { ImageConfig: parsedImageConfig }), - ...(snapStart && { SnapStart: parsedSnapStart }), - ...(loggingConfig && { LoggingConfig: parsedLoggingConfig }), - ...(tags && { Tags: parsedTags }), - ...(kmsKeyArn && { KMSKeyArn: kmsKeyArn }), - ...(codeSigningConfigArn && { CodeSigningConfigArn: codeSigningConfigArn }), - }; + const input = { + FunctionName: functionName, + Code: codeParameter, + ...(runtime && { Runtime: runtime }), + ...(role && { Role: role }), + ...(handler && { Handler: handler }), + ...(functionDescription && { Description: functionDescription }), + ...(parsedMemorySize && { MemorySize: parsedMemorySize }), + ...(timeout && { Timeout: timeout }), + ...(publish !== undefined && { Publish: publish }), + ...(architectures && { Architectures: Array.isArray(architectures) ? architectures : [architectures] }), + ...(ephemeralStorage && { EphemeralStorage: { Size: ephemeralStorage } }), + ...(revisionId && { RevisionId: revisionId }), + ...(vpcConfig && { VpcConfig: parsedVpcConfig }), + Environment: { Variables: parsedEnvironment }, + ...(deadLetterConfig && { DeadLetterConfig: parsedDeadLetterConfig }), + ...(tracingConfig && { TracingConfig: parsedTracingConfig }), + ...(layers && { Layers: parsedLayers }), + ...(fileSystemConfigs && { FileSystemConfigs: parsedFileSystemConfigs }), + ...(imageConfig && { ImageConfig: parsedImageConfig }), + ...(snapStart && { SnapStart: parsedSnapStart }), + ...(loggingConfig && { LoggingConfig: parsedLoggingConfig }), + ...(tags && { Tags: parsedTags }), + ...(kmsKeyArn && { KMSKeyArn: kmsKeyArn }), + ...(codeSigningConfigArn && { CodeSigningConfigArn: codeSigningConfigArn }), + }; - core.info(`Creating new Lambda function: ${functionName}`); - const command = new CreateFunctionCommand(input); - const response = await client.send(command); + core.info(`Creating new Lambda function: ${functionName}`); + const command = new CreateFunctionCommand(input); + const response = await client.send(command); - core.setOutput('function-arn', response.FunctionArn); - if (response.Version) { - core.setOutput('version', response.Version); - } + core.setOutput('function-arn', response.FunctionArn); + if (response.Version) { + core.setOutput('version', response.Version); + } - core.info('Lambda function created successfully'); + core.info('Lambda function created successfully'); - core.info(`Waiting for function ${functionName} to become active before proceeding`); - await waitForFunctionActive(client, functionName); - } catch (error) { - if (error.name === 'ThrottlingException' || error.name === 'TooManyRequestsException' || error.$metadata?.httpStatusCode === 429) { - core.setFailed(`Rate limit exceeded and maximum retries reached: ${error.message}`); - } else if (error.$metadata?.httpStatusCode >= 500) { - core.setFailed(`Server error (${error.$metadata?.httpStatusCode}): ${error.message}. All retry attempts failed.`); - } else if (error.name === 'AccessDeniedException') { - core.setFailed(`Action failed with error: Permissions error: ${error.message}. Check IAM roles.`); - } else { - core.setFailed(`Failed to create function: ${error.message}`); - } + core.info(`Waiting for function ${functionName} to become active before proceeding`); + await waitForFunctionActive(client, functionName); + } catch (error) { + if (error.name === 'ThrottlingException' || error.name === 'TooManyRequestsException' || error.$metadata?.httpStatusCode === 429) { + core.setFailed(`Rate limit exceeded and maximum retries reached: ${error.message}`); + } else if (error.$metadata?.httpStatusCode >= 500) { + core.setFailed(`Server error (${error.$metadata?.httpStatusCode}): ${error.message}. All retry attempts failed.`); + } else if (error.name === 'AccessDeniedException') { + core.setFailed(`Action failed with error: Permissions error: ${error.message}. Check IAM roles.`); + } else { + core.setFailed(`Failed to create function: ${error.message}`); + } - if (error.stack) { - core.debug(error.stack); - } - throw error; + if (error.stack) { + core.debug(error.stack); } + throw error; } + } } async function waitForFunctionActive(client, functionName, waitForMinutes = 5) { @@ -518,7 +490,6 @@ async function updateFunctionConfiguration(client, params) { const input = { FunctionName: functionName, ...(role && { Role: role }), - // Handler, Runtime, and Layers should not be included for Image package type ...(handler && { Handler: handler }), ...(functionDescription && { Description: functionDescription }), ...(parsedMemorySize && { MemorySize: parsedMemorySize }), @@ -530,9 +501,8 @@ async function updateFunctionConfiguration(client, params) { Environment: { Variables: parsedEnvironment }, ...(deadLetterConfig && { DeadLetterConfig: parsedDeadLetterConfig }), ...(tracingConfig && { TracingConfig: parsedTracingConfig }), - ...(parsedLayers && { Layers: parsedLayers }), + ...(layers && { Layers: parsedLayers }), ...(fileSystemConfigs && { FileSystemConfigs: parsedFileSystemConfigs }), - // ImageConfig should only be included for Image package type ...(imageConfig && { ImageConfig: parsedImageConfig }), ...(snapStart && { SnapStart: parsedSnapStart }), ...(loggingConfig && { LoggingConfig: parsedLoggingConfig }) @@ -587,7 +557,7 @@ async function waitForFunctionUpdated(client, functionName, waitForMinutes = 5) throw new Error(`Function ${functionName} not found`); } else if (error.$metadata && error.$metadata.httpStatusCode === 403) { throw new Error(`Permission denied while checking function ${functionName} status`); - } else if (error.message && error.message.includes("currently in the following state: 'Pending'")) { + } else if (error.message && error.message.includes('currently in the following state: \'Pending\'')) { core.warning(`Function ${functionName} is in 'Pending' state. Waiting for it to become active...`); await waitForFunctionActive(client, functionName, waitForMinutes); core.info(`Function ${functionName} is now active`); @@ -601,73 +571,62 @@ async function waitForFunctionUpdated(client, functionName, waitForMinutes = 5) // Helper function for updating Lambda function code async function updateFunctionCode(client, params) { const { - functionName, packageType, imageUri, finalZipPath, useS3Method, s3Bucket, s3Key, + functionName, finalZipPath, useS3Method, s3Bucket, s3Key, codeArtifactsDir, architectures, publish, revisionId, sourceKmsKeyArn, dryRun, region } = params; - core.info(`Updating function code for ${functionName}`); + core.info(`Updating function code for ${functionName} with ${finalZipPath}`); try { const commonCodeParams = { FunctionName: functionName, ...(architectures && { Architectures: Array.isArray(architectures) ? architectures : [architectures] }), ...(publish !== undefined && { Publish: publish }), - ...(revisionId && { RevisionId: revisionId }) + ...(revisionId && { RevisionId: revisionId }), + ...(sourceKmsKeyArn && { SourceKmsKeyArn: sourceKmsKeyArn }) }; let codeInput; - if (packageType === 'Image') { - // For container images, use ImageUri - core.info(`Using container image: ${imageUri}`); + if (useS3Method) { + core.info(`Using S3 deployment method with bucket: ${s3Bucket}, key: ${s3Key}`); + + await uploadToS3(finalZipPath, s3Bucket, s3Key, region); + core.info(`Successfully uploaded package to S3: s3://${s3Bucket}/${s3Key}`); + codeInput = { ...commonCodeParams, - ImageUri: imageUri + S3Bucket: s3Bucket, + S3Key: s3Key }; } else { - // For Zip packages, handle S3 or direct upload - if (useS3Method) { - core.info(`Using S3 deployment method with bucket: ${s3Bucket}, key: ${s3Key}`); - - await uploadToS3(finalZipPath, s3Bucket, s3Key, region); - core.info(`Successfully uploaded package to S3: s3://${s3Bucket}/${s3Key}`); - - codeInput = { - ...commonCodeParams, - S3Bucket: s3Bucket, - S3Key: s3Key, - ...(sourceKmsKeyArn && { SourceKmsKeyArn: sourceKmsKeyArn }) - }; - } else { - let zipFileContent; - - try { - zipFileContent = await fs.readFile(finalZipPath); - } catch (error) { - core.setFailed(`Failed to read Lambda deployment package at ${finalZipPath}: ${error.message}`); - - if (error.code === 'ENOENT') { - core.error(`File not found. Ensure the code artifacts directory "${codeArtifactsDir}" contains the required files.`); - } else if (error.code === 'EACCES') { - core.error('Permission denied. Check file access permissions.'); - } + let zipFileContent; - if (error.stack) { - core.debug(error.stack); - } + try { + zipFileContent = await fs.readFile(finalZipPath); + } catch (error) { + core.setFailed(`Failed to read Lambda deployment package at ${finalZipPath}: ${error.message}`); - throw error; + if (error.code === 'ENOENT') { + core.error(`File not found. Ensure the code artifacts directory "${codeArtifactsDir}" contains the required files.`); + } else if (error.code === 'EACCES') { + core.error('Permission denied. Check file access permissions.'); } - codeInput = { - ...commonCodeParams, - ZipFile: zipFileContent, - ...(sourceKmsKeyArn && { SourceKmsKeyArn: sourceKmsKeyArn }) - }; + if (error.stack) { + core.debug(error.stack); + } - core.info(`Original buffer length: ${zipFileContent.length} bytes`); + throw error; } + + codeInput = { + ...commonCodeParams, + ZipFile: zipFileContent + }; + + core.info(`Original buffer length: ${zipFileContent.length} bytes`); } if (dryRun) { @@ -1018,9 +977,9 @@ async function uploadToS3(zipFilePath, bucketName, s3Key, region) { try { const s3Client = new S3Client({ - region, + region, customUserAgent: `LambdaGitHubAction/${version}` - }); + }); let bucketExists = false; try { bucketExists = await checkBucketExists(s3Client, bucketName); @@ -1042,7 +1001,7 @@ async function uploadToS3(zipFilePath, bucketName, s3Key, region) { core.info(`Bucket ${bucketName} created successfully.`); } catch (bucketError) { core.error(`Failed to create bucket ${bucketName}: ${bucketError.message}`); - core.debug(bucketError.stack || "Bucket error stack trace"); + core.debug(bucketError.stack || 'Bucket error stack trace'); core.error(`Error details: ${JSON.stringify({ code: bucketError.code, name: bucketError.name, @@ -1073,11 +1032,10 @@ async function uploadToS3(zipFilePath, bucketName, s3Key, region) { core.info(`Read deployment package, size: ${fileContent.length} bytes`); try { - - expectedBucketOwner = await getAwsAccountId(region); + const expectedBucketOwner = await getAwsAccountId(region); if(!expectedBucketOwner) { - throw new Error("No AWS account ID found."); + throw new Error('No AWS account ID found.'); } const input = { @@ -98322,42 +98280,42 @@ function validateJsonInputs() { if (vpcConfig) { parsedVpcConfig = parseJsonInput(vpcConfig, 'vpc-config'); if (!parsedVpcConfig.SubnetIds || !Array.isArray(parsedVpcConfig.SubnetIds)) { - throw new Error("vpc-config must include 'SubnetIds' as an array"); + throw new Error('vpc-config must include \'SubnetIds\' as an array'); } if (!parsedVpcConfig.SecurityGroupIds || !Array.isArray(parsedVpcConfig.SecurityGroupIds)) { - throw new Error("vpc-config must include 'SecurityGroupIds' as an array"); + throw new Error('vpc-config must include \'SecurityGroupIds\' as an array'); } } if (deadLetterConfig) { parsedDeadLetterConfig = parseJsonInput(deadLetterConfig, 'dead-letter-config'); if (!parsedDeadLetterConfig.TargetArn) { - throw new Error("dead-letter-config must include 'TargetArn'"); + throw new Error('dead-letter-config must include \'TargetArn\''); } } if (tracingConfig) { parsedTracingConfig = parseJsonInput(tracingConfig, 'tracing-config'); if (!parsedTracingConfig.Mode || !['Active', 'PassThrough'].includes(parsedTracingConfig.Mode)) { - throw new Error("tracing-config Mode must be 'Active' or 'PassThrough'"); + throw new Error('tracing-config Mode must be \'Active\' or \'PassThrough\''); } } if (layers) { parsedLayers = parseJsonInput(layers, 'layers'); if (!Array.isArray(parsedLayers)) { - throw new Error("layers must be an array of layer ARNs"); + throw new Error('layers must be an array of layer ARNs'); } } if (fileSystemConfigs) { parsedFileSystemConfigs = parseJsonInput(fileSystemConfigs, 'file-system-configs'); if (!Array.isArray(parsedFileSystemConfigs)) { - throw new Error("file-system-configs must be an array"); + throw new Error('file-system-configs must be an array'); } for (const config of parsedFileSystemConfigs) { if (!config.Arn || !config.LocalMountPath) { - throw new Error("Each file-system-config must include 'Arn' and 'LocalMountPath'"); + throw new Error('Each file-system-config must include \'Arn\' and \'LocalMountPath\''); } } } @@ -98369,7 +98327,7 @@ function validateJsonInputs() { if (snapStart) { parsedSnapStart = parseJsonInput(snapStart, 'snap-start'); if (!parsedSnapStart.ApplyOn || !['PublishedVersions', 'None'].includes(parsedSnapStart.ApplyOn)) { - throw new Error("snap-start ApplyOn must be 'PublishedVersions' or 'None'"); + throw new Error('snap-start ApplyOn must be \'PublishedVersions\' or \'None\''); } } @@ -98380,7 +98338,7 @@ function validateJsonInputs() { if (tags) { parsedTags = parseJsonInput(tags, 'tags'); if (typeof parsedTags !== 'object' || Array.isArray(parsedTags)) { - throw new Error("tags must be an object of key-value pairs"); + throw new Error('tags must be an object of key-value pairs'); } } } catch (error) { @@ -98451,7 +98409,7 @@ function parseJsonInput(jsonString, inputName) { } function validateRoleArn(arn) { - const rolePattern = /^arn:aws(-[a-z0-9-]+)?:iam::[0-9]{12}:role\/[a-zA-Z0-9+=,.@_\/-]+$/; + const rolePattern = /^arn:aws(-[a-z0-9-]+)?:iam::[0-9]{12}:role\/[a-zA-Z0-9+=,.@_/-]+$/; if (!rolePattern.test(arn)) { core.setFailed(`Invalid IAM role ARN format: ${arn}`); @@ -98495,7 +98453,7 @@ function validateAndResolvePath(userPath, basePath) { } function checkInputConflicts(packageType, additionalInputs) { - const { s3Bucket, s3Key, useS3Method } = additionalInputs; + const { s3Bucket, s3Key } = additionalInputs; const sourceKmsKeyArn = core.getInput('source-kms-key-arn', { required: false }); if (packageType === 'Image') { @@ -100556,7 +100514,7 @@ module.exports = JSON.parse('{"name":"@aws-sdk/nested-clients","version":"3.826. /***/ ((module) => { "use strict"; -module.exports = JSON.parse('{"name":"@amzn/github-action-lambda-deploy","version":"1.0.0","description":"GitHub Action for AWS Lambda Function Deployment","main":"index.js","scripts":{"build":"ncc build index.js -o dist","test":"jest","lint":"eslint .","lint:fix":"eslint . --fix"},"keywords":["aws","lambda","deployment"],"author":"","license":"MIT","dependencies":{"@actions/core":"^1.10.0","@actions/github":"^6.0.1","@aws-sdk/client-lambda":"^3.826.0","@aws-sdk/client-s3":"^3.864.0","@aws-sdk/util-retry":"^3.370.0","@smithy/node-http-handler":"^4.0.6","@aws-sdk/client-sts":"3.864.0","adm-zip":"^0.5.16","glob":"^11.0.2"},"devDependencies":{"@vercel/ncc":"^0.36.1","eslint":"^8.45.0","eslint-plugin-jest":"^27.2.2","jest":"^29.5.0"}}'); +module.exports = JSON.parse('{"name":"@amzn/github-action-lambda-deploy","version":"1.0.0","description":"GitHub Action for AWS Lambda Function Deployment","main":"index.js","scripts":{"build":"ncc build index.js -o dist","test":"jest","lint":"eslint . --ignore-pattern \'dist/*\'","lint:fix":"eslint . --fix --ignore-pattern \'dist/*\'"},"keywords":["aws","lambda","deployment"],"author":"","license":"MIT","dependencies":{"@actions/core":"^1.10.0","@actions/github":"^6.0.1","@aws-sdk/client-lambda":"^3.826.0","@aws-sdk/client-s3":"^3.864.0","@aws-sdk/util-retry":"^3.370.0","@smithy/node-http-handler":"^4.0.6","@aws-sdk/client-sts":"3.864.0","adm-zip":"^0.5.16","glob":"^11.0.2"},"devDependencies":{"@vercel/ncc":"^0.36.1","eslint":"^8.45.0","eslint-plugin-jest":"^27.2.2","jest":"^29.5.0"}}'); /***/ }) diff --git a/index.js b/index.js index cf4350db..086da08b 100644 --- a/index.js +++ b/index.js @@ -17,10 +17,10 @@ async function run() { } const { - functionName, packageType, codeArtifactsDir, imageUri, + functionName, codeArtifactsDir, ephemeralStorage, parsedMemorySize, timeout, role, codeSigningConfigArn, kmsKeyArn, sourceKmsKeyArn, - environment, vpcConfig, deadLetterConfig, tracingConfig, + vpcConfig, deadLetterConfig, tracingConfig, layers, fileSystemConfigs, imageConfig, snapStart, loggingConfig, tags, parsedEnvironment, parsedVpcConfig, parsedDeadLetterConfig, @@ -62,18 +62,13 @@ async function run() { } } - // Creating zip file (only for Zip package type) - let finalZipPath = null; - if (packageType === 'Zip') { - core.info(`Packaging code artifacts from ${codeArtifactsDir}`); - finalZipPath = await packageCodeArtifacts(codeArtifactsDir); - } else if (packageType === 'Image') { - core.info(`Using container image: ${imageUri}`); - } + // Creating zip file + core.info(`Packaging code artifacts from ${codeArtifactsDir}`); + let finalZipPath = await packageCodeArtifacts(codeArtifactsDir); // Create function await createFunction(client, { - functionName, packageType, region, finalZipPath, imageUri, dryRun, role, + functionName, region, finalZipPath, dryRun, role, s3Bucket, s3Key, sourceKmsKeyArn, runtime, handler, functionDescription, parsedMemorySize, timeout, publish, architectures, ephemeralStorage, @@ -90,30 +85,22 @@ async function run() { const configCommand = new GetFunctionConfigurationCommand({FunctionName: functionName}); let currentConfig = await client.send(configCommand); - // Check if package type is being changed (not supported by AWS) - if (currentConfig.PackageType && currentConfig.PackageType !== packageType) { - core.setFailed(`Cannot change package type of existing Lambda function from ${currentConfig.PackageType} to ${packageType}`); - return; - } - const configChanged = hasConfigurationChanged(currentConfig, { ...(role && { Role: role }), - // Only include handler, runtime, and layers for Zip package type - ...(packageType === 'Zip' && handler && { Handler: handler }), + ...(handler && { Handler: handler }), ...(functionDescription && { Description: functionDescription }), ...(parsedMemorySize && { MemorySize: parsedMemorySize }), ...(timeout && { Timeout: timeout }), - ...(packageType === 'Zip' && runtime && { Runtime: runtime }), + ...(runtime && { Runtime: runtime }), ...(kmsKeyArn && { KMSKeyArn: kmsKeyArn }), ...(ephemeralStorage && { EphemeralStorage: { Size: ephemeralStorage } }), ...(vpcConfig && { VpcConfig: parsedVpcConfig }), Environment: { Variables: parsedEnvironment }, ...(deadLetterConfig && { DeadLetterConfig: parsedDeadLetterConfig }), ...(tracingConfig && { TracingConfig: parsedTracingConfig }), - ...(packageType === 'Zip' && layers && { Layers: parsedLayers }), + ...(layers && { Layers: parsedLayers }), ...(fileSystemConfigs && { FileSystemConfigs: parsedFileSystemConfigs }), - // Only include ImageConfig for Image package type - ...(packageType === 'Image' && imageConfig && { ImageConfig: parsedImageConfig }), + ...(imageConfig && { ImageConfig: parsedImageConfig }), ...(snapStart && { SnapStart: parsedSnapStart }), ...(loggingConfig && { LoggingConfig: parsedLoggingConfig }) }); @@ -127,29 +114,28 @@ async function run() { await updateFunctionConfiguration(client, { functionName, role, - // Only include handler, runtime, and layers for Zip package type - ...(packageType === 'Zip' && { handler }), + handler, functionDescription, parsedMemorySize, timeout, - ...(packageType === 'Zip' && { runtime }), + runtime, kmsKeyArn, ephemeralStorage, vpcConfig, parsedEnvironment, deadLetterConfig, tracingConfig, - ...(packageType === 'Zip' && { layers }), + layers, fileSystemConfigs, - ...(packageType === 'Image' && { imageConfig }), + imageConfig, snapStart, loggingConfig, parsedVpcConfig, parsedDeadLetterConfig, parsedTracingConfig, - ...(packageType === 'Zip' && { parsedLayers }), + parsedLayers, parsedFileSystemConfigs, - ...(packageType === 'Image' && { parsedImageConfig }), + parsedImageConfig, parsedSnapStart, parsedLoggingConfig }); @@ -160,8 +146,6 @@ async function run() { // Update Function Code await updateFunctionCode(client, { functionName, - packageType, - imageUri, finalZipPath, useS3Method, s3Bucket, @@ -199,11 +183,12 @@ async function packageCodeArtifacts(artifactsDir) { const zipPath = path.join(require('os').tmpdir(), `lambda-function-${Date.now()}.zip`); try { - try { - await fs.rm(tempDir, { recursive: true, force: true }); - } catch (error) { - } + await fs.rm(tempDir, { recursive: true, force: true }); + } catch (e) { + core.info(`Failed to remove directory ${tempDir}, continuing: ${e}`); + } + try { await fs.mkdir(tempDir, { recursive: true }); const workingDir = process.cwd(); @@ -306,7 +291,7 @@ async function checkFunctionExists(client, functionName) { // Helper functions for creating Lambda function async function createFunction(client, inputs, functionExists) { const { - functionName, packageType, region, finalZipPath, imageUri, dryRun, role, s3Bucket, s3Key, + functionName, region, finalZipPath, dryRun, role, s3Bucket, s3Key, sourceKmsKeyArn, runtime, handler, functionDescription, parsedMemorySize, timeout, publish, architectures, ephemeralStorage, revisionId, vpcConfig, parsedEnvironment, deadLetterConfig, tracingConfig, @@ -317,133 +302,120 @@ async function createFunction(client, inputs, functionExists) { } = inputs; if (!functionExists) { - if (dryRun) { - core.setFailed('DRY RUN MODE can only be used for updating function code of existing functions'); - return; - } + if (dryRun) { + core.setFailed('DRY RUN MODE can only be used for updating function code of existing functions'); + return; + } - core.info(`Function ${functionName} doesn't exist, creating new function`); + core.info(`Function ${functionName} doesn't exist, creating new function`); - if(!role) { - core.setFailed('Role ARN must be provided when creating a new function'); - return; - } + if(!role) { + core.setFailed('Role ARN must be provided when creating a new function'); + return; + } - try { - core.info(`Creating Lambda function with ${packageType} package type`); + try { + core.info('Creating Lambda function with deployment package'); + + let codeParameter; - let codeParameter; + if (s3Bucket) { + try { + await uploadToS3(finalZipPath, s3Bucket, s3Key, region); + core.info(`Successfully uploaded package to S3: s3://${s3Bucket}/${s3Key}`); - if (packageType === 'Image') { - // For container images, use ImageUri - core.info(`Using container image: ${imageUri}`); codeParameter = { - ImageUri: imageUri + S3Bucket: s3Bucket, + S3Key: s3Key, + ...(sourceKmsKeyArn && { SourceKmsKeyArn: sourceKmsKeyArn }) }; - } else { - // For Zip packages, handle S3 or direct upload - if (s3Bucket) { - try { - await uploadToS3(finalZipPath, s3Bucket, s3Key, region); - core.info(`Successfully uploaded package to S3: s3://${s3Bucket}/${s3Key}`); - - codeParameter = { - S3Bucket: s3Bucket, - S3Key: s3Key, - ...(sourceKmsKeyArn && { SourceKmsKeyArn: sourceKmsKeyArn }) - }; - } catch (error) { - core.setFailed(`Failed to upload package to S3: ${error.message}`); - if (error.stack) { - core.debug(error.stack); - } - throw error; - } - } else { - try { - const zipFileContent = await fs.readFile(finalZipPath); - core.info(`Zip file read successfully, size: ${zipFileContent.length} bytes`); - - codeParameter = { - ZipFile: zipFileContent, - ...(sourceKmsKeyArn && { SourceKmsKeyArn: sourceKmsKeyArn }) - }; - } catch (error) { - if (error.code === 'EACCES') { - core.setFailed(`Failed to read Lambda deployment package: Permission denied`); - core.error('Permission denied. Check file access permissions.'); - } else { - core.setFailed(`Failed to read Lambda deployment package: ${error.message}`); - } - if (error.stack) { - core.debug(error.stack); - } - throw error; - } + } catch (error) { + core.setFailed(`Failed to upload package to S3: ${error.message}`); + if (error.stack) { + core.debug(error.stack); } + throw error; } + } else { + try { + const zipFileContent = await fs.readFile(finalZipPath); + core.info(`Zip file read successfully, size: ${zipFileContent.length} bytes`); - const input = { - FunctionName: functionName, - Code: codeParameter, - PackageType: packageType, - ...(role && { Role: role }), - // Only include runtime and handler for Zip packages - ...(packageType === 'Zip' && runtime && { Runtime: runtime }), - ...(packageType === 'Zip' && handler && { Handler: handler }), - ...(functionDescription && { Description: functionDescription }), - ...(parsedMemorySize && { MemorySize: parsedMemorySize }), - ...(timeout && { Timeout: timeout }), - ...(publish !== undefined && { Publish: publish }), - ...(architectures && { Architectures: Array.isArray(architectures) ? architectures : [architectures] }), - ...(ephemeralStorage && { EphemeralStorage: { Size: ephemeralStorage } }), - ...(revisionId && { RevisionId: revisionId }), - ...(vpcConfig && { VpcConfig: parsedVpcConfig }), - Environment: { Variables: parsedEnvironment }, - ...(deadLetterConfig && { DeadLetterConfig: parsedDeadLetterConfig }), - ...(tracingConfig && { TracingConfig: parsedTracingConfig }), - // Only include Layers for Zip package type - ...(packageType === 'Zip' && layers && { Layers: parsedLayers }), - ...(fileSystemConfigs && { FileSystemConfigs: parsedFileSystemConfigs }), - // Only include ImageConfig for Image package type - ...(packageType === 'Image' && imageConfig && { ImageConfig: parsedImageConfig }), - ...(snapStart && { SnapStart: parsedSnapStart }), - ...(loggingConfig && { LoggingConfig: parsedLoggingConfig }), - ...(tags && { Tags: parsedTags }), - ...(kmsKeyArn && { KMSKeyArn: kmsKeyArn }), - ...(codeSigningConfigArn && { CodeSigningConfigArn: codeSigningConfigArn }), - }; - - core.info(`Creating new Lambda function: ${functionName}`); - const command = new CreateFunctionCommand(input); - const response = await client.send(command); - - core.setOutput('function-arn', response.FunctionArn); - if (response.Version) { - core.setOutput('version', response.Version); + codeParameter = { + ZipFile: zipFileContent, + ...(sourceKmsKeyArn && { SourceKmsKeyArn: sourceKmsKeyArn }) + }; + } catch (error) { + if (error.code === 'EACCES') { + core.setFailed(`Failed to read Lambda deployment package: Permission denied`); + core.error('Permission denied. Check file access permissions.'); + } else { + core.setFailed(`Failed to read Lambda deployment package: ${error.message}`); + } + if (error.stack) { + core.debug(error.stack); + } + throw error; } + } - core.info('Lambda function created successfully'); + const input = { + FunctionName: functionName, + Code: codeParameter, + ...(runtime && { Runtime: runtime }), + ...(role && { Role: role }), + ...(handler && { Handler: handler }), + ...(functionDescription && { Description: functionDescription }), + ...(parsedMemorySize && { MemorySize: parsedMemorySize }), + ...(timeout && { Timeout: timeout }), + ...(publish !== undefined && { Publish: publish }), + ...(architectures && { Architectures: Array.isArray(architectures) ? architectures : [architectures] }), + ...(ephemeralStorage && { EphemeralStorage: { Size: ephemeralStorage } }), + ...(revisionId && { RevisionId: revisionId }), + ...(vpcConfig && { VpcConfig: parsedVpcConfig }), + Environment: { Variables: parsedEnvironment }, + ...(deadLetterConfig && { DeadLetterConfig: parsedDeadLetterConfig }), + ...(tracingConfig && { TracingConfig: parsedTracingConfig }), + ...(layers && { Layers: parsedLayers }), + ...(fileSystemConfigs && { FileSystemConfigs: parsedFileSystemConfigs }), + ...(imageConfig && { ImageConfig: parsedImageConfig }), + ...(snapStart && { SnapStart: parsedSnapStart }), + ...(loggingConfig && { LoggingConfig: parsedLoggingConfig }), + ...(tags && { Tags: parsedTags }), + ...(kmsKeyArn && { KMSKeyArn: kmsKeyArn }), + ...(codeSigningConfigArn && { CodeSigningConfigArn: codeSigningConfigArn }), + }; - core.info(`Waiting for function ${functionName} to become active before proceeding`); - await waitForFunctionActive(client, functionName); - } catch (error) { - if (error.name === 'ThrottlingException' || error.name === 'TooManyRequestsException' || error.$metadata?.httpStatusCode === 429) { - core.setFailed(`Rate limit exceeded and maximum retries reached: ${error.message}`); - } else if (error.$metadata?.httpStatusCode >= 500) { - core.setFailed(`Server error (${error.$metadata?.httpStatusCode}): ${error.message}. All retry attempts failed.`); - } else if (error.name === 'AccessDeniedException') { - core.setFailed(`Action failed with error: Permissions error: ${error.message}. Check IAM roles.`); - } else { - core.setFailed(`Failed to create function: ${error.message}`); - } + core.info(`Creating new Lambda function: ${functionName}`); + const command = new CreateFunctionCommand(input); + const response = await client.send(command); - if (error.stack) { - core.debug(error.stack); - } - throw error; + core.setOutput('function-arn', response.FunctionArn); + if (response.Version) { + core.setOutput('version', response.Version); } + + core.info('Lambda function created successfully'); + + core.info(`Waiting for function ${functionName} to become active before proceeding`); + await waitForFunctionActive(client, functionName); + } catch (error) { + if (error.name === 'ThrottlingException' || error.name === 'TooManyRequestsException' || error.$metadata?.httpStatusCode === 429) { + core.setFailed(`Rate limit exceeded and maximum retries reached: ${error.message}`); + } else if (error.$metadata?.httpStatusCode >= 500) { + core.setFailed(`Server error (${error.$metadata?.httpStatusCode}): ${error.message}. All retry attempts failed.`); + } else if (error.name === 'AccessDeniedException') { + core.setFailed(`Action failed with error: Permissions error: ${error.message}. Check IAM roles.`); + } else { + core.setFailed(`Failed to create function: ${error.message}`); + } + + if (error.stack) { + core.debug(error.stack); + } + throw error; } + } } async function waitForFunctionActive(client, functionName, waitForMinutes = 5) { @@ -512,7 +484,6 @@ async function updateFunctionConfiguration(client, params) { const input = { FunctionName: functionName, ...(role && { Role: role }), - // Handler, Runtime, and Layers should not be included for Image package type ...(handler && { Handler: handler }), ...(functionDescription && { Description: functionDescription }), ...(parsedMemorySize && { MemorySize: parsedMemorySize }), @@ -524,9 +495,8 @@ async function updateFunctionConfiguration(client, params) { Environment: { Variables: parsedEnvironment }, ...(deadLetterConfig && { DeadLetterConfig: parsedDeadLetterConfig }), ...(tracingConfig && { TracingConfig: parsedTracingConfig }), - ...(parsedLayers && { Layers: parsedLayers }), + ...(layers && { Layers: parsedLayers }), ...(fileSystemConfigs && { FileSystemConfigs: parsedFileSystemConfigs }), - // ImageConfig should only be included for Image package type ...(imageConfig && { ImageConfig: parsedImageConfig }), ...(snapStart && { SnapStart: parsedSnapStart }), ...(loggingConfig && { LoggingConfig: parsedLoggingConfig }) @@ -581,7 +551,7 @@ async function waitForFunctionUpdated(client, functionName, waitForMinutes = 5) throw new Error(`Function ${functionName} not found`); } else if (error.$metadata && error.$metadata.httpStatusCode === 403) { throw new Error(`Permission denied while checking function ${functionName} status`); - } else if (error.message && error.message.includes("currently in the following state: 'Pending'")) { + } else if (error.message && error.message.includes('currently in the following state: \'Pending\'')) { core.warning(`Function ${functionName} is in 'Pending' state. Waiting for it to become active...`); await waitForFunctionActive(client, functionName, waitForMinutes); core.info(`Function ${functionName} is now active`); @@ -595,73 +565,62 @@ async function waitForFunctionUpdated(client, functionName, waitForMinutes = 5) // Helper function for updating Lambda function code async function updateFunctionCode(client, params) { const { - functionName, packageType, imageUri, finalZipPath, useS3Method, s3Bucket, s3Key, + functionName, finalZipPath, useS3Method, s3Bucket, s3Key, codeArtifactsDir, architectures, publish, revisionId, sourceKmsKeyArn, dryRun, region } = params; - core.info(`Updating function code for ${functionName}`); + core.info(`Updating function code for ${functionName} with ${finalZipPath}`); try { const commonCodeParams = { FunctionName: functionName, ...(architectures && { Architectures: Array.isArray(architectures) ? architectures : [architectures] }), ...(publish !== undefined && { Publish: publish }), - ...(revisionId && { RevisionId: revisionId }) + ...(revisionId && { RevisionId: revisionId }), + ...(sourceKmsKeyArn && { SourceKmsKeyArn: sourceKmsKeyArn }) }; let codeInput; - if (packageType === 'Image') { - // For container images, use ImageUri - core.info(`Using container image: ${imageUri}`); + if (useS3Method) { + core.info(`Using S3 deployment method with bucket: ${s3Bucket}, key: ${s3Key}`); + + await uploadToS3(finalZipPath, s3Bucket, s3Key, region); + core.info(`Successfully uploaded package to S3: s3://${s3Bucket}/${s3Key}`); + codeInput = { ...commonCodeParams, - ImageUri: imageUri + S3Bucket: s3Bucket, + S3Key: s3Key }; } else { - // For Zip packages, handle S3 or direct upload - if (useS3Method) { - core.info(`Using S3 deployment method with bucket: ${s3Bucket}, key: ${s3Key}`); - - await uploadToS3(finalZipPath, s3Bucket, s3Key, region); - core.info(`Successfully uploaded package to S3: s3://${s3Bucket}/${s3Key}`); - - codeInput = { - ...commonCodeParams, - S3Bucket: s3Bucket, - S3Key: s3Key, - ...(sourceKmsKeyArn && { SourceKmsKeyArn: sourceKmsKeyArn }) - }; - } else { - let zipFileContent; + let zipFileContent; - try { - zipFileContent = await fs.readFile(finalZipPath); - } catch (error) { - core.setFailed(`Failed to read Lambda deployment package at ${finalZipPath}: ${error.message}`); - - if (error.code === 'ENOENT') { - core.error(`File not found. Ensure the code artifacts directory "${codeArtifactsDir}" contains the required files.`); - } else if (error.code === 'EACCES') { - core.error('Permission denied. Check file access permissions.'); - } - - if (error.stack) { - core.debug(error.stack); - } + try { + zipFileContent = await fs.readFile(finalZipPath); + } catch (error) { + core.setFailed(`Failed to read Lambda deployment package at ${finalZipPath}: ${error.message}`); - throw error; + if (error.code === 'ENOENT') { + core.error(`File not found. Ensure the code artifacts directory "${codeArtifactsDir}" contains the required files.`); + } else if (error.code === 'EACCES') { + core.error('Permission denied. Check file access permissions.'); } - codeInput = { - ...commonCodeParams, - ZipFile: zipFileContent, - ...(sourceKmsKeyArn && { SourceKmsKeyArn: sourceKmsKeyArn }) - }; + if (error.stack) { + core.debug(error.stack); + } - core.info(`Original buffer length: ${zipFileContent.length} bytes`); + throw error; } + + codeInput = { + ...commonCodeParams, + ZipFile: zipFileContent + }; + + core.info(`Original buffer length: ${zipFileContent.length} bytes`); } if (dryRun) { @@ -1012,9 +971,9 @@ async function uploadToS3(zipFilePath, bucketName, s3Key, region) { try { const s3Client = new S3Client({ - region, + region, customUserAgent: `LambdaGitHubAction/${version}` - }); + }); let bucketExists = false; try { bucketExists = await checkBucketExists(s3Client, bucketName); @@ -1036,7 +995,7 @@ async function uploadToS3(zipFilePath, bucketName, s3Key, region) { core.info(`Bucket ${bucketName} created successfully.`); } catch (bucketError) { core.error(`Failed to create bucket ${bucketName}: ${bucketError.message}`); - core.debug(bucketError.stack || "Bucket error stack trace"); + core.debug(bucketError.stack || 'Bucket error stack trace'); core.error(`Error details: ${JSON.stringify({ code: bucketError.code, name: bucketError.name, @@ -1067,11 +1026,10 @@ async function uploadToS3(zipFilePath, bucketName, s3Key, region) { core.info(`Read deployment package, size: ${fileContent.length} bytes`); try { - - expectedBucketOwner = await getAwsAccountId(region); + const expectedBucketOwner = await getAwsAccountId(region); if(!expectedBucketOwner) { - throw new Error("No AWS account ID found."); + throw new Error('No AWS account ID found.'); } const input = { diff --git a/package.json b/package.json index 2cce0058..d7279395 100644 --- a/package.json +++ b/package.json @@ -6,8 +6,8 @@ "scripts": { "build": "ncc build index.js -o dist", "test": "jest", - "lint": "eslint .", - "lint:fix": "eslint . --fix" + "lint": "eslint . --ignore-pattern 'dist/*'", + "lint:fix": "eslint . --fix --ignore-pattern 'dist/*'" }, "keywords": [ "aws", diff --git a/validations.js b/validations.js index fe83e927..4f8998ca 100644 --- a/validations.js +++ b/validations.js @@ -154,42 +154,42 @@ function validateJsonInputs() { if (vpcConfig) { parsedVpcConfig = parseJsonInput(vpcConfig, 'vpc-config'); if (!parsedVpcConfig.SubnetIds || !Array.isArray(parsedVpcConfig.SubnetIds)) { - throw new Error("vpc-config must include 'SubnetIds' as an array"); + throw new Error('vpc-config must include \'SubnetIds\' as an array'); } if (!parsedVpcConfig.SecurityGroupIds || !Array.isArray(parsedVpcConfig.SecurityGroupIds)) { - throw new Error("vpc-config must include 'SecurityGroupIds' as an array"); + throw new Error('vpc-config must include \'SecurityGroupIds\' as an array'); } } if (deadLetterConfig) { parsedDeadLetterConfig = parseJsonInput(deadLetterConfig, 'dead-letter-config'); if (!parsedDeadLetterConfig.TargetArn) { - throw new Error("dead-letter-config must include 'TargetArn'"); + throw new Error('dead-letter-config must include \'TargetArn\''); } } if (tracingConfig) { parsedTracingConfig = parseJsonInput(tracingConfig, 'tracing-config'); if (!parsedTracingConfig.Mode || !['Active', 'PassThrough'].includes(parsedTracingConfig.Mode)) { - throw new Error("tracing-config Mode must be 'Active' or 'PassThrough'"); + throw new Error('tracing-config Mode must be \'Active\' or \'PassThrough\''); } } if (layers) { parsedLayers = parseJsonInput(layers, 'layers'); if (!Array.isArray(parsedLayers)) { - throw new Error("layers must be an array of layer ARNs"); + throw new Error('layers must be an array of layer ARNs'); } } if (fileSystemConfigs) { parsedFileSystemConfigs = parseJsonInput(fileSystemConfigs, 'file-system-configs'); if (!Array.isArray(parsedFileSystemConfigs)) { - throw new Error("file-system-configs must be an array"); + throw new Error('file-system-configs must be an array'); } for (const config of parsedFileSystemConfigs) { if (!config.Arn || !config.LocalMountPath) { - throw new Error("Each file-system-config must include 'Arn' and 'LocalMountPath'"); + throw new Error('Each file-system-config must include \'Arn\' and \'LocalMountPath\''); } } } @@ -201,7 +201,7 @@ function validateJsonInputs() { if (snapStart) { parsedSnapStart = parseJsonInput(snapStart, 'snap-start'); if (!parsedSnapStart.ApplyOn || !['PublishedVersions', 'None'].includes(parsedSnapStart.ApplyOn)) { - throw new Error("snap-start ApplyOn must be 'PublishedVersions' or 'None'"); + throw new Error('snap-start ApplyOn must be \'PublishedVersions\' or \'None\''); } } @@ -212,7 +212,7 @@ function validateJsonInputs() { if (tags) { parsedTags = parseJsonInput(tags, 'tags'); if (typeof parsedTags !== 'object' || Array.isArray(parsedTags)) { - throw new Error("tags must be an object of key-value pairs"); + throw new Error('tags must be an object of key-value pairs'); } } } catch (error) { @@ -283,7 +283,7 @@ function parseJsonInput(jsonString, inputName) { } function validateRoleArn(arn) { - const rolePattern = /^arn:aws(-[a-z0-9-]+)?:iam::[0-9]{12}:role\/[a-zA-Z0-9+=,.@_\/-]+$/; + const rolePattern = /^arn:aws(-[a-z0-9-]+)?:iam::[0-9]{12}:role\/[a-zA-Z0-9+=,.@_/-]+$/; if (!rolePattern.test(arn)) { core.setFailed(`Invalid IAM role ARN format: ${arn}`); @@ -327,7 +327,7 @@ function validateAndResolvePath(userPath, basePath) { } function checkInputConflicts(packageType, additionalInputs) { - const { s3Bucket, s3Key, useS3Method } = additionalInputs; + const { s3Bucket, s3Key } = additionalInputs; const sourceKmsKeyArn = core.getInput('source-kms-key-arn', { required: false }); if (packageType === 'Image') {