diff --git a/README.md b/README.md index 146ba9fe..1a78ea7a 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,11 @@ jobs: # Add any additional inputs your action supports ``` -The required parameters to deploy are function name, code artifacts directory, handler, and runtime. The function name and code artifacts directory need to be provided by the user. However, the handler and runtime do not and will default to index.handler and nodejs20.x if not provided. +The action automatically updates your Lambda function code when the required parameters are provided. If the function doesn't exist, it will be created first and then updated. + +Required parameters include function name, code artifacts directory, handler, and runtime. While function name and code artifacts directory must be specified, handler and runtime will default to `index.handler` and `nodejs20.x` respectively if not provided. + +The following examples demonstrate additional features available: ### Update Function Configuration @@ -178,7 +182,8 @@ Here's an example of using OIDC with the aws-actions/configure-aws-credentials a - name: Configure AWS credentials with OIDC uses: aws-actions/configure-aws-credentials@v2 with: - role-to-assume: arn:aws:iam::123456789012:role/GitHubActionRole + role-to-assume: my-role + aws-region: my-region ``` To use OIDC authentication, you must configure a trust policy in AWS IAM that allows GitHub Actions to assume an IAM role. Here's an example trust policy: diff --git a/__tests__/function_create.test.js b/__tests__/function_create.test.js index aa1e6421..4327fbe2 100644 --- a/__tests__/function_create.test.js +++ b/__tests__/function_create.test.js @@ -82,6 +82,19 @@ jest.mock('@aws-sdk/client-s3', () => { })) }; }); +jest.mock('@aws-sdk/client-sts', () => { + const original = jest.requireActual('@aws-sdk/client-sts'); + return { + ...original, + GetCallerIdentityCommand: jest.fn().mockImplementation((params) => ({ + ...params, + type: 'GetCallerIdentityCommand' + })), + STSClient: jest.fn().mockImplementation(() => ({ + send: jest.fn().mockResolvedValue({ Account: '123456789012' }) + })) + }; +}); afterAll(() => { jest.clearAllMocks(); @@ -598,4 +611,148 @@ describe('Function Existence Check', () => { })); }); }); + + test('Config changed with dry run logs message', () => { + const configChanged = true; + const dryRun = true; + + if (configChanged) { + if (dryRun) { + core.info('[DRY RUN] Configuration updates are not simulated in dry run mode'); + return; + } + } + + expect(core.info).toHaveBeenCalledWith('[DRY RUN] Configuration updates are not simulated in dry run mode'); + }); + + test('Config changed without dry run calls updateFunctionConfiguration', async () => { + const mockClient = { send: jest.fn() }; + const configChanged = true; + const dryRun = false; + + if (configChanged) { + if (dryRun) { + core.info('[DRY RUN] Configuration updates are not simulated in dry run mode'); + return; + } + + await index.updateFunctionConfiguration(mockClient, { + functionName: 'test-function', + role: 'test-role', + handler: 'index.handler', + functionDescription: 'test', + parsedMemorySize: 256, + timeout: 30, + runtime: 'nodejs20.x', + kmsKeyArn: 'test-kms', + ephemeralStorage: 512, + vpcConfig: '{}', + parsedEnvironment: {}, + deadLetterConfig: '{}', + tracingConfig: '{}', + layers: '[]', + fileSystemConfigs: '[]', + imageConfig: '{}', + snapStart: '{}', + loggingConfig: '{}', + parsedVpcConfig: {}, + parsedDeadLetterConfig: {}, + parsedTracingConfig: {}, + parsedLayers: [], + parsedFileSystemConfigs: [], + parsedImageConfig: {}, + parsedSnapStart: {}, + parsedLoggingConfig: {} + }); + } + + expect(mockClient.send).toHaveBeenCalled(); + }); + + test('No config changes logs no changes message', () => { + const configChanged = false; + + if (configChanged) { + // Should not execute + } else { + core.info('No configuration changes detected'); + } + + expect(core.info).toHaveBeenCalledWith('No configuration changes detected'); + }); + + test('generateS3Key includes commit hash when GITHUB_SHA exists', () => { + process.env.GITHUB_SHA = 'abcdef1234567890'; + const result = index.generateS3Key('test-function'); + expect(result).toContain('-abcdef1'); + delete process.env.GITHUB_SHA; + }); + + test('validateBucketName validates bucket names', () => { + expect(index.validateBucketName('valid-bucket-name')).toBe(true); + expect(index.validateBucketName('ab')).toBe(false); + expect(index.validateBucketName('INVALID')).toBe(false); + expect(index.validateBucketName('192.168.1.1')).toBe(false); + expect(index.validateBucketName('bucket..name')).toBe(false); + }); + + test('isEmptyValue checks empty values', () => { + expect(index.isEmptyValue(null)).toBe(true); + expect(index.isEmptyValue('')).toBe(true); + expect(index.isEmptyValue([])).toBe(true); + expect(index.isEmptyValue({})).toBe(true); + expect(index.isEmptyValue('value')).toBe(false); + expect(index.isEmptyValue({ SubnetIds: [] })).toBe(false); + }); + + test('cleanNullKeys removes null values', () => { + expect(index.cleanNullKeys(null)).toBeUndefined(); + expect(index.cleanNullKeys('')).toBeUndefined(); + expect(index.cleanNullKeys({ key: 'value', empty: null })).toEqual({ key: 'value' }); + expect(index.cleanNullKeys([])).toBeUndefined(); + expect(index.cleanNullKeys(['value', null])).toEqual(['value']); + }); + + test('deepEqual compares objects deeply', () => { + expect(index.deepEqual({ a: 1 }, { a: 1 })).toBe(true); + expect(index.deepEqual({ a: 1 }, { a: 2 })).toBe(false); + expect(index.deepEqual([1, 2], [1, 2])).toBe(true); + expect(index.deepEqual([1, 2], [1, 3])).toBe(false); + expect(index.deepEqual(null, null)).toBe(true); + expect(index.deepEqual('test', 'test')).toBe(true); + }); + + test('hasConfigurationChanged detects changes', async () => { + const current = { Role: 'old-role', Handler: 'old.handler' }; + const updated = { Role: 'new-role', Handler: 'old.handler' }; + const result = await index.hasConfigurationChanged(current, updated); + expect(result).toBe(true); + expect(core.info).toHaveBeenCalledWith(expect.stringContaining('Configuration difference detected')); + }); + + test('hasConfigurationChanged returns true for empty current config', async () => { + const result = await index.hasConfigurationChanged({}, { Role: 'test' }); + expect(result).toBe(true); + }); + + test('getAwsAccountId retrieves account ID', async () => { + const mockSend = jest.fn().mockResolvedValue({ Account: '123456789012' }); + const { STSClient } = require('@aws-sdk/client-sts'); + STSClient.mockImplementation(() => ({ send: mockSend })); + + const result = await index.getAwsAccountId('us-east-1'); + expect(result).toBe('123456789012'); + expect(core.info).toHaveBeenCalledWith('Successfully retrieved AWS account ID: 123456789012'); + }); + + test('getAwsAccountId handles errors', async () => { + const mockSend = jest.fn().mockRejectedValue(new Error('STS error')); + const { STSClient } = require('@aws-sdk/client-sts'); + STSClient.mockImplementation(() => ({ send: mockSend })); + + const result = await index.getAwsAccountId('us-east-1'); + expect(result).toBeNull(); + expect(core.warning).toHaveBeenCalledWith('Failed to retrieve AWS account ID: STS error'); + }); });